org.openmrs.module.openconceptlab.updater.Importer.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.openconceptlab.updater.Importer.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
 *
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
 * graphic logo is a trademark of OpenMRS Inc.
 */
package org.openmrs.module.openconceptlab.updater;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.Concept;
import org.openmrs.ConceptAnswer;
import org.openmrs.ConceptClass;
import org.openmrs.ConceptDatatype;
import org.openmrs.ConceptDescription;
import org.openmrs.ConceptMap;
import org.openmrs.ConceptMapType;
import org.openmrs.ConceptName;
import org.openmrs.ConceptNumeric;
import org.openmrs.ConceptReferenceTerm;
import org.openmrs.ConceptSet;
import org.openmrs.ConceptSource;
import org.openmrs.api.ConceptNameType;
import org.openmrs.api.ConceptService;
import org.openmrs.api.DuplicateConceptNameException;
import org.openmrs.api.context.Context;
import org.openmrs.module.openconceptlab.CacheService;
import org.openmrs.module.openconceptlab.Item;
import org.openmrs.module.openconceptlab.ItemState;
import org.openmrs.module.openconceptlab.Update;
import org.openmrs.module.openconceptlab.UpdateService;
import org.openmrs.module.openconceptlab.client.OclConcept;
import org.openmrs.module.openconceptlab.client.OclConcept.Description;
import org.openmrs.module.openconceptlab.client.OclConcept.Extras;
import org.openmrs.module.openconceptlab.client.OclMapping;
import org.openmrs.module.openconceptlab.client.OclMapping.MapType;

public class Importer {

    protected final Log log = LogFactory.getLog(getClass());

    private static final Object CREATE_CONCEPT_REFERENCE_TERM_LOCK = new Object();

    private static final Object CREATE_CONCEPT_CLASS_LOCK = new Object();

    private static final Object CREATE_CONCEPT_SOURCE_LOCK = new Object();

    ConceptService conceptService;

    UpdateService updateService;

    public void setConceptService(ConceptService conceptService) {
        this.conceptService = conceptService;
    }

    public void setUpdateService(UpdateService updateService) {
        this.updateService = updateService;
    }

    /**
     * @param oclConcept
     * @param importQueue
     * @throws ImportException
     * @should save new concept
     * @should add new names to concept
     * @should update name type in concept
     * @should update names with different uuids
     * @should void names from concept
     * @should add new descriptions to concept
     * @should void descriptions from concept
     * @should retire concept
     * @should unretire concept
     * @should fail if datatype missing
     * @should create concept class if missing
     * @should change duplicate synonym to index term
     * @should change duplicate fully specified name to index term
     * @should skip updating concept if it is already up to date
     */
    public Item importConcept(CacheService cacheService, Update update, OclConcept oclConcept)
            throws ImportException {
        Item item = updateService.getLastSuccessfulItemByUrl(oclConcept.getUrl());
        if (item != null && item.getVersionUrl().equals(oclConcept.getVersionUrl())) {
            return new Item(update, oclConcept, ItemState.UP_TO_DATE);
        }

        Concept concept = toConcept(cacheService, oclConcept);

        boolean added = false;
        if (concept.getId() == null) {
            added = true;
        }

        boolean retryOnFailure = true;
        boolean retryOnDuplicateNames = true;
        List<String> resolutionLog = new ArrayList<String>();
        while (true) {
            try {
                try {
                    conceptService.saveConcept(concept);

                    return new Item(update, oclConcept, added ? ItemState.ADDED : ItemState.UPDATED);
                } catch (DuplicateConceptNameException e) {
                    if (!retryOnDuplicateNames) {
                        throw new ImportException("Cannot import concept " + concept.getUuid() + ", tried:\n"
                                + logToString(resolutionLog), e);
                    }

                    Context.clearSession();
                    cacheService.clearCache();
                    update = updateService.getUpdate(update.getUpdateId());
                    concept = toConcept(cacheService, oclConcept);

                    resolutionLog.add("Fixing duplicate names for concept " + concept.getUuid()
                            + " after failure due to " + e.getMessage());
                    changeDuplicateNamesToIndexTerms(concept, resolutionLog);
                    resolutionLog.add("Done fixing duplicate names for concept " + concept.getUuid());

                    retryOnDuplicateNames = false;
                }
            } catch (ImportException e) {
                throw e; //re-throw, failed to resolved duplicates
            } catch (Exception e) {
                if (!retryOnFailure) {
                    throw new ImportException("Cannot import concept " + concept.getUuid() + ", tried:\n"
                            + logToString(resolutionLog), e);
                }

                Context.clearSession();
                cacheService.clearCache();
                update = updateService.getUpdate(update.getUpdateId());
                concept = toConcept(cacheService, oclConcept);

                resolutionLog.add("Retrying import of concept " + concept.getUuid() + " after failure due to '"
                        + e.getMessage() + "'");

                retryOnFailure = false;
            }
        }
    }

    public String logToString(List<String> log) {
        return StringUtils.join(log, "\n");
    }

    public Concept toConcept(CacheService cacheService, OclConcept oclConcept) throws ImportException {
        ConceptDatatype datatype = cacheService.getConceptDatatypeByName(oclConcept.getDatatype());
        if (datatype == null) {
            throw new ImportException("Datatype '" + oclConcept.getDatatype() + "' is not supported by OpenMRS");
        }

        Concept concept = cacheService.getConceptByUuid(oclConcept.getExternalId());
        if (concept == null) {
            if (datatype.getUuid().equals(ConceptDatatype.NUMERIC_UUID)) {
                concept = new ConceptNumeric();
            } else {
                concept = new Concept();
            }
            concept.setUuid(oclConcept.getExternalId());
        }
        ConceptClass conceptClass = cacheService.getConceptClassByName(oclConcept.getConceptClass());
        if (conceptClass == null) {
            synchronized (CREATE_CONCEPT_CLASS_LOCK) {
                conceptClass = cacheService.getConceptClassByName(oclConcept.getConceptClass());
                if (conceptClass == null) {
                    conceptClass = new ConceptClass();
                    conceptClass.setName(oclConcept.getConceptClass());
                    conceptClass.setDescription("Imported from Open Concept Lab");
                    conceptService.saveConceptClass(conceptClass);
                }
            }
        }
        concept.setConceptClass(conceptClass);

        concept.setDatatype(datatype);

        if (concept instanceof ConceptNumeric) {
            ConceptNumeric numeric = (ConceptNumeric) concept;

            Extras extras = oclConcept.getExtras();

            numeric.setHiAbsolute(extras.getHiAbsolute());

            numeric.setHiCritical(extras.getHiCritical());

            numeric.setHiNormal(extras.getHiNormal());

            numeric.setLowAbsolute(extras.getLowAbsolute());

            numeric.setLowCritical(extras.getLowCritical());

            numeric.setLowNormal(extras.getLowNormal());

            numeric.setUnits(extras.getUnits());

            numeric.setPrecise(extras.getPrecise());
        }

        concept.setRetired(oclConcept.isRetired());
        if (oclConcept.isRetired()) {
            concept.setRetireReason("Retired in OCL");
        } else {
            concept.setRetireReason(null);
            concept.setRetiredBy(null);
        }

        voidNamesRemovedFromOcl(concept, oclConcept);

        updateOrAddNamesFromOcl(concept, oclConcept);

        removeDescriptionsRemovedFromOcl(concept, oclConcept);

        addDescriptionsFromOcl(concept, oclConcept);

        return concept;
    }

    private void changeDuplicateNamesToIndexTerms(Concept concept, List<String> resolutionLog) {
        List<ConceptName> conceptNames = updateService.changeDuplicateConceptNamesToIndexTerms(concept);
        for (ConceptName conceptName : conceptNames) {
            resolutionLog.add("Changed name '" + conceptName.getName() + "' in locale " + conceptName.getLocale()
                    + " to index term");
        }

        //Make sure there is at least one fully specified name
        boolean hasFullySpecifiedName = false;
        for (ConceptName name : concept.getNames()) {
            if (ConceptNameType.FULLY_SPECIFIED.equals(name.getConceptNameType())) {
                hasFullySpecifiedName = true;
                break;
            }
        }

        if (!hasFullySpecifiedName) {
            for (ConceptName name : concept.getNames()) {
                if (!ConceptNameType.INDEX_TERM.equals(name.getConceptNameType())) {
                    name.setConceptNameType(ConceptNameType.FULLY_SPECIFIED);
                    break;
                }
            }
        }
    }

    /**
     * @param update
     * @param oclMapping
     * @return
     * @should add concept answer
     * @should add concept set member
     * @should remove concept answer
     * @should remove concept set member
     * @should add concept mapping and term
     * @should add concept mapping and unretire term
     * @should remove concept mapping and retire term
     * @should update mapping only if it has been updated since last import
     */
    public Item importMapping(CacheService cacheService, Update update, OclMapping oclMapping) {
        Item oldMappingItem = updateService.getLastSuccessfulItemByUrl(oclMapping.getUrl());
        if (oldMappingItem != null && isMappingUpToDate(oldMappingItem, oclMapping)) {
            return new Item(update, oclMapping, ItemState.UP_TO_DATE);
        }
        ;

        final Item item;

        Item fromItem = null;
        Concept fromConcept = null;
        if (!StringUtils.isBlank(oclMapping.getFromConceptUrl())) {
            fromItem = updateService.getLastSuccessfulItemByUrl(oclMapping.getFromConceptUrl());
            if (fromItem != null) {
                fromConcept = cacheService.getConceptByUuid(fromItem.getUuid());
            }

            if (fromConcept == null) {
                return new Item(update, oclMapping, ItemState.ERROR, "Cannot create mapping from concept with URL "
                        + oclMapping.getFromConceptUrl() + ", because the concept has not been imported");
            }
        }

        Item toItem = null;
        Concept toConcept = null;
        if (!StringUtils.isBlank(oclMapping.getToConceptUrl())) {
            toItem = updateService.getLastSuccessfulItemByUrl(oclMapping.getToConceptUrl());
            if (toItem != null) {
                toConcept = cacheService.getConceptByUuid(toItem.getUuid());
            }

            if (toConcept == null) {
                return new Item(update, oclMapping, ItemState.ERROR, "Cannot create mapping to concept with URL "
                        + oclMapping.getToConceptUrl() + ", because the concept has not been imported");
            }
        }

        if (MapType.Q_AND_A.equals(oclMapping.getMapType()) || MapType.SET.equals(oclMapping.getMapType())) {
            if (oclMapping.getMapType().equals(MapType.Q_AND_A)) {
                item = updateOrAddAnswersFromOcl(update, oclMapping, fromConcept, toConcept);
            } else {
                item = updateOrAddSetMemebersFromOcl(update, oclMapping, fromConcept, toConcept);
            }

            updateService.updateConceptWithoutValidation(fromConcept);
        } else {
            ConceptSource toSource = cacheService.getConceptSourceByName(oclMapping.getToSourceName());
            if (toSource == null) {
                synchronized (CREATE_CONCEPT_SOURCE_LOCK) {
                    toSource = cacheService.getConceptSourceByName(oclMapping.getToSourceName());
                    if (toSource == null) {
                        toSource = new ConceptSource();
                        toSource.setName(oclMapping.getToSourceName());
                        toSource.setDescription("Imported from " + oclMapping.getUrl());
                        conceptService.saveConceptSource(toSource);
                    }
                }
            }

            String mapTypeName = oclMapping.getMapType().replace("-", "_");
            ConceptMapType mapType = cacheService.getConceptMapTypeByName(mapTypeName);
            if (mapType == null) {
                return new Item(update, oclMapping, ItemState.ERROR, "Map type " + mapTypeName + " does not exist");
            }

            if (fromConcept != null) {
                ConceptMap conceptMap = updateService.getConceptMapByUuid(oclMapping.getExternalId());

                ConceptReferenceTerm term = createOrUpdateConceptReferenceTerm(oclMapping, conceptMap, toSource);

                if (conceptMap != null) {
                    if (!conceptMap.getConcept().equals(fromConcept)) {
                        //Concept changed, it would be unusual, but still probable
                        Concept previousConcept = conceptMap.getConcept();

                        previousConcept.removeConceptMapping(conceptMap);
                        updateService.updateConceptWithoutValidation(previousConcept);

                        fromConcept.addConceptMapping(conceptMap);
                    }

                    if (Boolean.TRUE.equals(oclMapping.getRetired())) {
                        item = new Item(update, oclMapping, ItemState.RETIRED);
                        fromConcept.removeConceptMapping(conceptMap);
                    } else {
                        item = new Item(update, oclMapping, ItemState.UPDATED);
                        conceptMap.setConceptMapType(mapType);
                    }
                } else {
                    conceptMap = new ConceptMap();
                    conceptMap.setUuid(oclMapping.getExternalId());
                    conceptMap.setConceptReferenceTerm(term);
                    conceptMap.setConceptMapType(mapType);
                    fromConcept.addConceptMapping(conceptMap);

                    item = new Item(update, oclMapping, ItemState.ADDED);
                }

                updateService.updateConceptWithoutValidation(fromConcept);
            } else {
                return new Item(update, oclMapping, ItemState.ERROR,
                        "Mapping " + oclMapping.getUrl() + " is not supported");
            }
        }
        return item;
    }

    /**
     * @param oldItem
     * @param newMapping
     * @return boolean
     * @should should return true if any of updatedOn is null
     * @should should return false if both updatedOn are null
     * @should should return if mapping's updatedOn is after
     */

    public boolean isMappingUpToDate(Item oldItem, OclMapping newMapping) {
        Date oldUpdatedOn = oldItem.getUpdatedOn();
        Date newUpdatedOn = newMapping.getUpdatedOn();
        //mapping never was updated
        if (oldUpdatedOn == null && newUpdatedOn == null) {
            return true;
        }
        //mapping was updated at least once
        else if (oldUpdatedOn != null && newUpdatedOn != null) {
            return newUpdatedOn.equals(oldUpdatedOn);
        }
        //this is first update - old version updatedOn is null
        else
            return false;
    }

    ConceptReferenceTerm createOrUpdateConceptReferenceTerm(OclMapping oclMapping, ConceptMap conceptMap,
            ConceptSource toSource) {
        ConceptReferenceTerm term = null;
        if (conceptMap == null) {
            term = conceptService.getConceptReferenceTermByCode(oclMapping.getToConceptCode(), toSource);
        } else {
            term = conceptMap.getConceptReferenceTerm();
        }

        if (term == null) {
            synchronized (CREATE_CONCEPT_REFERENCE_TERM_LOCK) {
                term = conceptService.getConceptReferenceTermByCode(oclMapping.getToConceptCode(), toSource);

                if (term == null) {
                    term = new ConceptReferenceTerm();
                    term.setConceptSource(toSource);
                    term.setCode(oclMapping.getToConceptCode());

                    conceptService.saveConceptReferenceTerm(term);
                }
            }
        }

        if (term.isRetired() != oclMapping.isRetired()) {
            term.setRetired(oclMapping.isRetired());
            if (oclMapping.isRetired()) {
                term.setRetireReason("OCL subscription");
            } else {
                term.setRetireReason(null);
                term.setRetiredBy(null);
            }

            updateService.updateConceptReferenceTermWithoutValidation(term);
        }

        return term;
    }

    Item updateOrAddSetMemebersFromOcl(Update update, OclMapping oclMapping, Concept set, Concept member) {
        Item item = null;

        boolean found = false;
        Iterator<ConceptSet> it = set.getConceptSets().iterator();
        while (it.hasNext()) {
            ConceptSet conceptSet = it.next();

            if (conceptSet.getUuid().equals(oclMapping.getExternalId())) {
                found = true;

                if (Boolean.TRUE.equals(oclMapping.getRetired())) {
                    item = new Item(update, oclMapping, ItemState.RETIRED);
                    it.remove();
                } else {
                    item = new Item(update, oclMapping, ItemState.UPDATED);
                    conceptSet.setConcept(member);
                }

                break;
            }
        }

        if (!found) {
            ConceptSet conceptSet = new ConceptSet();
            conceptSet.setConceptSet(set);
            conceptSet.setConcept(member);
            conceptSet.setUuid(oclMapping.getExternalId());
            conceptSet.setSortWeight(1.0);
            set.getConceptSets().add(conceptSet);
            item = new Item(update, oclMapping, ItemState.ADDED);
        }

        return item;
    }

    Item updateOrAddAnswersFromOcl(Update update, OclMapping oclMapping, Concept question, Concept answer) {
        Item item = null;

        boolean found = false;
        Iterator<ConceptAnswer> it = question.getAnswers().iterator();
        while (it.hasNext()) {
            ConceptAnswer conceptAnswer = it.next();

            if (conceptAnswer.getUuid().equals(oclMapping.getExternalId())) {
                found = true;

                if (Boolean.TRUE.equals(oclMapping.getRetired())) {
                    item = new Item(update, oclMapping, ItemState.RETIRED);
                    it.remove();
                } else {
                    item = new Item(update, oclMapping, ItemState.UPDATED);
                    conceptAnswer.setAnswerConcept(answer);
                }

                break;
            }
        }

        if (!found) {
            ConceptAnswer conceptAnswer = new ConceptAnswer(answer);
            conceptAnswer.setUuid(oclMapping.getExternalId());
            question.addAnswer(conceptAnswer);
            item = new Item(update, oclMapping, ItemState.ADDED);
        }

        return item;
    }

    void updateOrAddNamesFromOcl(Concept concept, OclConcept oclConcept) {
        for (OclConcept.Name oclName : oclConcept.getNames()) {
            ConceptNameType oclNameType = oclName.getNameType() != null
                    ? ConceptNameType.valueOf(oclName.getNameType())
                    : null;

            boolean nameFound = false;
            for (ConceptName name : concept.getNames(true)) {
                if (isMatch(oclName, name)) {
                    //Let's make sure all is the same
                    name.setName(oclName.getName());
                    name.setLocale(oclName.getLocale());
                    name.setConceptNameType(oclNameType);
                    name.setLocalePreferred(oclName.isLocalePreferred());

                    //Unvoiding if necessary
                    name.setVoided(false);
                    name.setVoidReason(null);
                    name.setVoidedBy(null);

                    nameFound = true;
                    break;
                }
            }

            if (!nameFound) {
                ConceptName name = new ConceptName(oclName.getName(), oclName.getLocale());
                name.setUuid(oclName.getExternalId());
                name.setConceptNameType(oclNameType);
                name.setLocalePreferred(oclName.isLocalePreferred());

                concept.addName(name);
            }
        }
    }

    void voidNamesRemovedFromOcl(Concept concept, OclConcept oclConcept) {
        for (ConceptName name : concept.getNames(true)) {
            boolean nameFound = false;
            for (OclConcept.Name oclName : oclConcept.getNames()) {
                if (isMatch(oclName, name)) {
                    nameFound = true;
                    break;
                }
            }
            if (!nameFound && !name.isVoided()) {
                name.setVoided(true);
                name.setVoidReason("Removed from OCL");
            }
        }
    }

    void addDescriptionsFromOcl(Concept concept, OclConcept oclConcept) {
        for (OclConcept.Description oclDescription : oclConcept.getDescriptions()) {
            if (StringUtils.isBlank(oclDescription.getDescription())) {
                continue;
            }

            boolean nameFound = false;
            for (ConceptDescription description : concept.getDescriptions()) {
                if (isMatch(oclDescription, description)) {
                    //Let's make sure all is the same
                    description.setDescription(oclDescription.getDescription());
                    description.setLocale(oclDescription.getLocale());
                    nameFound = true;
                    break;
                }
            }

            if (!nameFound) {
                ConceptDescription description = new ConceptDescription(oclDescription.getDescription(),
                        oclDescription.getLocale());
                description.setUuid(oclDescription.getExternalId());
                concept.addDescription(description);
            }
        }
    }

    void removeDescriptionsRemovedFromOcl(Concept concept, OclConcept oclConcept) {
        for (Iterator<ConceptDescription> it = concept.getDescriptions().iterator(); it.hasNext();) {
            ConceptDescription description = it.next();
            boolean descriptionFound = false;
            for (Description oclDescription : oclConcept.getDescriptions()) {
                if (isMatch(oclDescription, description)) {
                    if (!StringUtils.isBlank(oclDescription.getDescription())) {
                        //Blank descriptions are invalid and will be removed
                        descriptionFound = true;
                    }
                    break;
                }
            }
            if (!descriptionFound) {
                it.remove();
            }
        }
    }

    public boolean isMatch(OclConcept.Name oclName, ConceptName name) {
        return new EqualsBuilder().append(name.getUuid(), oclName.getExternalId()).isEquals();
    }

    public boolean isMatch(OclConcept.Description oclDescription, ConceptDescription description) {
        return new EqualsBuilder().append(description.getUuid(), oclDescription.getExternalId()).isEquals();
    }
}