de.symeda.sormas.ui.importer.CaseImporter.java Source code

Java tutorial

Introduction

Here is the source code for de.symeda.sormas.ui.importer.CaseImporter.java

Source

/*******************************************************************************
 * SORMAS - Surveillance Outbreak Response Management & Analysis System
 * Copyright  2016-2018 Helmholtz-Zentrum fr Infektionsforschung GmbH (HZI)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *******************************************************************************/
package de.symeda.sormas.ui.importer;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;

import com.vaadin.server.Sizeable.Unit;
import com.vaadin.ui.Button;
import com.vaadin.ui.UI;

import de.symeda.sormas.api.FacadeProvider;
import de.symeda.sormas.api.caze.CaseCriteria;
import de.symeda.sormas.api.caze.CaseDataDto;
import de.symeda.sormas.api.caze.CaseIndexDto;
import de.symeda.sormas.api.caze.CaseSimilarityCriteria;
import de.symeda.sormas.api.facility.FacilityReferenceDto;
import de.symeda.sormas.api.i18n.Captions;
import de.symeda.sormas.api.i18n.I18nProperties;
import de.symeda.sormas.api.i18n.Strings;
import de.symeda.sormas.api.i18n.Validations;
import de.symeda.sormas.api.importexport.InvalidColumnException;
import de.symeda.sormas.api.person.PersonDto;
import de.symeda.sormas.api.person.PersonReferenceDto;
import de.symeda.sormas.api.region.CommunityReferenceDto;
import de.symeda.sormas.api.region.DistrictReferenceDto;
import de.symeda.sormas.api.region.RegionReferenceDto;
import de.symeda.sormas.api.user.UserDto;
import de.symeda.sormas.api.user.UserReferenceDto;
import de.symeda.sormas.api.utils.DateHelper;
import de.symeda.sormas.api.utils.ValidationRuntimeException;
import de.symeda.sormas.ui.utils.CommitDiscardWrapperComponent;
import de.symeda.sormas.ui.utils.CommitDiscardWrapperComponent.CommitListener;
import de.symeda.sormas.ui.utils.CommitDiscardWrapperComponent.DiscardListener;
import de.symeda.sormas.ui.utils.VaadinUiUtil;

/**
 * These are the steps performed by the case importer:
 * 
 * 1) Read the CSV file from the passed file path and open an error report file
 * 2) Read the header row from the CSV and build a list of properties based on its columns
 * 3) Read the next line from the CSV and fill a case with its contents by using reflection;
 *    Validate the case afterwards
 *      - If an error is thrown doing this, the import of this case is canceled and the case gets
 *      added to the error report file
 * 4) Check the database for similar cases and, if at least one is found, execute the 
 *    similarityCallback received by the calling class.
 *    - The import will wait for the similarityCallback to be resolved before it is continued
 * 5) Based on the results of the similarityCallback, an existing case might be overridden by 
 *      the data in the CSV file
 * 6) Save the person and case to the database (unless the case was skipped or the import
 *    was canceled)
 * 7) Repeat from step 3 until all cases have been handled
 */
public class CaseImporter extends DataImporter {

    public CaseImporter(File inputFile, UserReferenceDto currentUser, UI currentUI) throws IOException {
        this(inputFile, null, currentUser, currentUI);
    }

    public CaseImporter(File inputFile, OutputStreamWriter errorReportWriter, UserReferenceDto currentUser,
            UI currentUI) throws IOException {
        super(inputFile, errorReportWriter, currentUser, currentUI);

    }

    @Override
    protected void importDataFromCsvLine(String[] nextLine, String[] headersLine, List<String[]> headers)
            throws IOException, InvalidColumnException, InterruptedException {
        // Check whether the new line has the same length as the header line
        if (nextLine.length > headersLine.length) {
            hasImportError = true;
            writeImportError(nextLine, I18nProperties.getValidationError(Validations.importLineTooLong));
            readNextLineFromCsv(headersLine, headers);
        }

        final PersonDto newPersonTmp = PersonDto.build();
        final CaseDataDto newCaseTmp = CaseDataDto.build(newPersonTmp.toReference(), null);
        newCaseTmp.setReportingUser(currentUser);

        boolean caseHasImportError = insertRowIntoData(nextLine, headers, false,
                new BiFunction<String, String[], Exception>() {
                    @Override
                    public Exception apply(String entry, String[] entryHeaderPath) {
                        try {
                            insertColumnEntryIntoData(newCaseTmp, newPersonTmp, entry, entryHeaderPath);
                        } catch (ImportErrorException | InvalidColumnException e) {
                            return e;
                        }

                        return null;
                    }
                });

        CaseDataDto newCase = newCaseTmp;
        PersonDto newPerson = newPersonTmp;

        if (!caseHasImportError) {
            try {
                FacadeProvider.getPersonFacade().validate(newPerson);
                FacadeProvider.getCaseFacade().validate(newCase);
            } catch (ValidationRuntimeException e) {
                hasImportError = true;
                caseHasImportError = true;
                writeImportError(nextLine, e.getMessage());
            }
        }

        if (!caseHasImportError) {
            try {
                CaseImportConsumer consumer = new CaseImportConsumer();
                ImportSimilarityResultOption resultOption = null;

                CaseImportLock LOCK = new CaseImportLock();
                synchronized (LOCK) {
                    CaseCriteria caseCriteria = new CaseCriteria().disease(newCase.getDisease())
                            .region(newCase.getRegion());
                    CaseSimilarityCriteria criteria = new CaseSimilarityCriteria()
                            .firstName(newPerson.getFirstName()).lastName(newPerson.getLastName())
                            .caseCriteria(caseCriteria).reportDate(newCase.getReportDate());
                    List<CaseIndexDto> similarCases = FacadeProvider.getCaseFacade().getSimilarCases(criteria,
                            currentUser.getUuid());

                    if (similarCases.size() > 0) {
                        handleSimilarity(new ImportSimilarityInput(newCase, newPerson, similarCases),
                                new Consumer<ImportSimilarityResult>() {
                                    @Override
                                    public void accept(ImportSimilarityResult result) {
                                        consumer.onImportResult(result, LOCK);
                                    }
                                });

                        try {
                            if (!LOCK.wasNotified) {
                                LOCK.wait();
                            }
                        } catch (InterruptedException e) {
                            logger.error("InterruptedException when trying to perform LOCK.wait() in case import: "
                                    + e.getMessage());
                            throw e;
                        }

                        if (consumer.result != null) {
                            resultOption = consumer.result.getResultOption();
                        }

                        if (resultOption != null && resultOption != ImportSimilarityResultOption.SKIP
                                && resultOption != ImportSimilarityResultOption.CANCEL
                                && resultOption != ImportSimilarityResultOption.PICK) {
                            if (resultOption == ImportSimilarityResultOption.OVERRIDE
                                    && consumer.result.getMatchingCase() != null) {
                                final CaseDataDto matchingCaseTmp = FacadeProvider.getCaseFacade()
                                        .getCaseDataByUuid(consumer.result.getMatchingCase().getUuid());
                                final PersonDto matchingCasePersonTmp = FacadeProvider.getPersonFacade()
                                        .getPersonByUuid(matchingCaseTmp.getPerson().getUuid());
                                caseHasImportError = insertRowIntoData(nextLine, headers, true,
                                        new BiFunction<String, String[], Exception>() {
                                            @Override
                                            public Exception apply(String entry, String[] entryHeaderPath) {
                                                try {
                                                    insertColumnEntryIntoData(matchingCaseTmp,
                                                            matchingCasePersonTmp, entry, entryHeaderPath);
                                                } catch (ImportErrorException | InvalidColumnException e) {
                                                    return e;
                                                }

                                                return null;
                                            }
                                        });

                                newCase = matchingCaseTmp;
                                newPerson = matchingCasePersonTmp;
                            }
                        }
                    }
                }

                if (caseHasImportError) {
                    // In case insertRowIntoCase when matching person/case has thrown an unexpected error
                    importedCallback.accept(ImportResult.ERROR);
                    readNextLineFromCsv(headersLine, headers);
                } else if (resultOption != null && resultOption == ImportSimilarityResultOption.SKIP) {
                    // Reset the import result
                    consumer.result = null;
                    importedCallback.accept(ImportResult.SKIPPED);
                    readNextLineFromCsv(headersLine, headers);
                } else if (resultOption != null && resultOption == ImportSimilarityResultOption.PICK) {
                    consumer.result = null;
                    importedCallback.accept(ImportResult.DUPLICATE);
                    readNextLineFromCsv(headersLine, headers);
                } else if (resultOption != null && resultOption == ImportSimilarityResultOption.CANCEL) {
                    cancelAfterCurrent = true;
                    return;
                } else {
                    PersonDto savedPerson = FacadeProvider.getPersonFacade().savePerson(newPerson);
                    newCase.setPerson(savedPerson.toReference());
                    FacadeProvider.getCaseFacade().saveCase(newCase);
                    // Reset the import result
                    consumer.result = null;
                    importedCallback.accept(ImportResult.SUCCESS);
                    readNextLineFromCsv(headersLine, headers);
                }
            } catch (ValidationRuntimeException e) {
                hasImportError = true;
                writeImportError(nextLine, e.getMessage());
                importedCallback.accept(ImportResult.ERROR);
                readNextLineFromCsv(headersLine, headers);
            }
        } else {
            importedCallback.accept(ImportResult.ERROR);
            readNextLineFromCsv(headersLine, headers);
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private void insertColumnEntryIntoData(CaseDataDto caze, PersonDto person, String entry,
            String[] entryHeaderPath) throws InvalidColumnException, ImportErrorException {
        Object currentElement = caze;
        for (int i = 0; i < entryHeaderPath.length; i++) {
            String headerPathElementName = entryHeaderPath[i];

            try {
                if (i != entryHeaderPath.length - 1) {
                    currentElement = new PropertyDescriptor(headerPathElementName, currentElement.getClass())
                            .getReadMethod().invoke(currentElement);
                    // Replace PersonReferenceDto with the created person
                    if (currentElement instanceof PersonReferenceDto) {
                        currentElement = person;
                    }
                } else {
                    PropertyDescriptor pd = new PropertyDescriptor(headerPathElementName,
                            currentElement.getClass());
                    Class<?> propertyType = pd.getPropertyType();

                    if (propertyType.isEnum()) {
                        pd.getWriteMethod().invoke(currentElement,
                                Enum.valueOf((Class<? extends Enum>) propertyType, entry.toUpperCase()));
                    } else if (propertyType.isAssignableFrom(Date.class)) {
                        pd.getWriteMethod().invoke(currentElement, DateHelper.parseDateWithException(entry));
                    } else if (propertyType.isAssignableFrom(Integer.class)) {
                        pd.getWriteMethod().invoke(currentElement, Integer.parseInt(entry));
                    } else if (propertyType.isAssignableFrom(Double.class)) {
                        pd.getWriteMethod().invoke(currentElement, Double.parseDouble(entry));
                    } else if (propertyType.isAssignableFrom(Float.class)) {
                        pd.getWriteMethod().invoke(currentElement, Float.parseFloat(entry));
                    } else if (propertyType.isAssignableFrom(Boolean.class)) {
                        pd.getWriteMethod().invoke(currentElement, Boolean.parseBoolean(entry));
                    } else if (propertyType.isAssignableFrom(RegionReferenceDto.class)) {
                        List<RegionReferenceDto> region = FacadeProvider.getRegionFacade().getByName(entry);
                        if (region.isEmpty()) {
                            throw new ImportErrorException(
                                    I18nProperties.getValidationError(Validations.importEntryDoesNotExist, entry,
                                            buildHeaderPathString(entryHeaderPath)));
                        } else if (region.size() > 1) {
                            throw new ImportErrorException(
                                    I18nProperties.getValidationError(Validations.importRegionNotUnique, entry,
                                            buildHeaderPathString(entryHeaderPath)));
                        } else {
                            pd.getWriteMethod().invoke(currentElement, region.get(0));
                        }
                    } else if (propertyType.isAssignableFrom(DistrictReferenceDto.class)) {
                        List<DistrictReferenceDto> district = FacadeProvider.getDistrictFacade().getByName(entry,
                                caze.getRegion());
                        if (district.isEmpty()) {
                            throw new ImportErrorException(
                                    I18nProperties.getValidationError(Validations.importEntryDoesNotExistDbOrRegion,
                                            entry, buildHeaderPathString(entryHeaderPath)));
                        } else if (district.size() > 1) {
                            throw new ImportErrorException(
                                    I18nProperties.getValidationError(Validations.importDistrictNotUnique, entry,
                                            buildHeaderPathString(entryHeaderPath)));
                        } else {
                            pd.getWriteMethod().invoke(currentElement, district.get(0));
                        }
                    } else if (propertyType.isAssignableFrom(CommunityReferenceDto.class)) {
                        List<CommunityReferenceDto> community = FacadeProvider.getCommunityFacade().getByName(entry,
                                caze.getDistrict());
                        if (community.isEmpty()) {
                            throw new ImportErrorException(I18nProperties.getValidationError(
                                    Validations.importEntryDoesNotExistDbOrDistrict, entry,
                                    buildHeaderPathString(entryHeaderPath)));
                        } else if (community.size() > 1) {
                            throw new ImportErrorException(
                                    I18nProperties.getValidationError(Validations.importCommunityNotUnique, entry,
                                            buildHeaderPathString(entryHeaderPath)));
                        } else {
                            pd.getWriteMethod().invoke(currentElement, community.get(0));
                        }
                    } else if (propertyType.isAssignableFrom(FacilityReferenceDto.class)) {
                        List<FacilityReferenceDto> facility = FacadeProvider.getFacilityFacade().getByName(entry,
                                caze.getDistrict(), caze.getCommunity());
                        if (facility.isEmpty()) {
                            if (caze.getCommunity() != null) {
                                throw new ImportErrorException(I18nProperties.getValidationError(
                                        Validations.importEntryDoesNotExistDbOrCommunity, entry,
                                        buildHeaderPathString(entryHeaderPath)));
                            } else {
                                throw new ImportErrorException(I18nProperties.getValidationError(
                                        Validations.importEntryDoesNotExistDbOrDistrict, entry,
                                        buildHeaderPathString(entryHeaderPath)));
                            }
                        } else if (facility.size() > 1 && caze.getCommunity() == null) {
                            throw new ImportErrorException(
                                    I18nProperties.getValidationError(Validations.importFacilityNotUniqueInDistrict,
                                            entry, buildHeaderPathString(entryHeaderPath)));
                        } else if (facility.size() > 1 && caze.getCommunity() != null) {
                            throw new ImportErrorException(I18nProperties.getValidationError(
                                    Validations.importFacilityNotUniqueInCommunity, entry,
                                    buildHeaderPathString(entryHeaderPath)));
                        } else {
                            pd.getWriteMethod().invoke(currentElement, facility.get(0));
                        }
                    } else if (propertyType.isAssignableFrom(UserReferenceDto.class)) {
                        UserDto user = FacadeProvider.getUserFacade().getByUserName(entry);
                        if (user != null) {
                            pd.getWriteMethod().invoke(currentElement, user.toReference());
                        } else {
                            throw new ImportErrorException(
                                    I18nProperties.getValidationError(Validations.importEntryDoesNotExist, entry,
                                            buildHeaderPathString(entryHeaderPath)));
                        }
                    } else if (propertyType.isAssignableFrom(String.class)) {
                        pd.getWriteMethod().invoke(currentElement, entry);
                    } else {
                        throw new UnsupportedOperationException(I18nProperties.getValidationError(
                                Validations.importCasesPropertyTypeNotAllowed, propertyType.getName()));
                    }
                }
            } catch (IntrospectionException e) {
                throw new InvalidColumnException(buildHeaderPathString(entryHeaderPath));
            } catch (InvocationTargetException | IllegalAccessException e) {
                throw new ImportErrorException(I18nProperties.getValidationError(Validations.importErrorInColumn,
                        buildHeaderPathString(entryHeaderPath)));
            } catch (IllegalArgumentException e) {
                throw new ImportErrorException(entry, buildHeaderPathString(entryHeaderPath));
            } catch (ParseException e) {
                throw new ImportErrorException(I18nProperties.getValidationError(Validations.importInvalidDate,
                        buildHeaderPathString(entryHeaderPath)));
            } catch (ImportErrorException e) {
                throw e;
            } catch (Exception e) {
                logger.error("Unexpected error when trying to import a case: " + e.getMessage());
                throw new ImportErrorException(
                        I18nProperties.getValidationError(Validations.importCasesUnexpectedError));
            }
        }
    }

    private void handleSimilarity(ImportSimilarityInput input, Consumer<ImportSimilarityResult> resultConsumer) {
        currentUI.accessSynchronously(new Runnable() {
            @Override
            public void run() {
                CasePickOrImportField pickOrImportField = new CasePickOrImportField(input.getCaze(),
                        input.getPerson(), input.getSimilarCases());
                pickOrImportField.setWidth(1024, Unit.PIXELS);

                final CommitDiscardWrapperComponent<CasePickOrImportField> component = new CommitDiscardWrapperComponent<>(
                        pickOrImportField);

                component.addCommitListener(new CommitListener() {
                    @Override
                    public void onCommit() {
                        CaseIndexDto pickedCase = pickOrImportField.getValue();
                        if (pickedCase != null) {
                            if (pickOrImportField.isOverrideCase()) {
                                resultConsumer.accept(new ImportSimilarityResult(pickedCase,
                                        ImportSimilarityResultOption.OVERRIDE));
                            } else {
                                resultConsumer.accept(
                                        new ImportSimilarityResult(pickedCase, ImportSimilarityResultOption.PICK));
                            }
                        } else {
                            // TODO May be wrong here!
                            resultConsumer
                                    .accept(new ImportSimilarityResult(null, ImportSimilarityResultOption.CREATE));
                        }
                    }
                });

                DiscardListener discardListener = new DiscardListener() {
                    @Override
                    public void onDiscard() {
                        resultConsumer
                                .accept(new ImportSimilarityResult(null, ImportSimilarityResultOption.CANCEL));
                    }
                };
                component.addDiscardListener(discardListener);
                component.getDiscardButton().setCaption(I18nProperties.getCaption(Captions.actionCancel));
                component.getCommitButton().setCaption(I18nProperties.getCaption(Captions.actionConfirm));

                Button skipButton = new Button(I18nProperties.getCaption(Captions.actionSkip));
                skipButton.addClickListener(e -> {
                    currentUI.accessSynchronously(new Runnable() {
                        @Override
                        public void run() {
                            component.removeDiscardListener(discardListener);
                            component.discard();
                            resultConsumer
                                    .accept(new ImportSimilarityResult(null, ImportSimilarityResultOption.SKIP));
                        }
                    });
                });
                component.getButtonsPanel().addComponentAsFirst(skipButton);

                pickOrImportField.setSelectionChangeCallback((commitAllowed) -> {
                    component.getCommitButton().setEnabled(commitAllowed);
                });

                VaadinUiUtil.showModalPopupWindow(component,
                        I18nProperties.getString(Strings.headingPickOrCreateCase));
            }
        });
    }

    private class CaseImportConsumer {
        protected ImportSimilarityResult result;

        private void onImportResult(ImportSimilarityResult result, CaseImportLock LOCK) {
            this.result = result;
            synchronized (LOCK) {
                LOCK.notify();
                LOCK.wasNotified = true;
            }
        }
    }

    private class CaseImportLock {
        protected boolean wasNotified = false;
    }

}