All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ehrbase.service.CompositionServiceImp Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2024 vitasystems GmbH.
 *
 * This file is part of project EHRbase
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.ehrbase.service;

import static org.ehrbase.repository.AbstractVersionedObjectRepository.buildObjectVersionId;
import static org.ehrbase.repository.AbstractVersionedObjectRepository.extractUid;
import static org.ehrbase.repository.AbstractVersionedObjectRepository.extractVersion;

import com.nedap.archie.rm.changecontrol.OriginalVersion;
import com.nedap.archie.rm.composition.Composition;
import com.nedap.archie.rm.ehr.VersionedComposition;
import com.nedap.archie.rm.generic.Attestation;
import com.nedap.archie.rm.generic.AuditDetails;
import com.nedap.archie.rm.generic.RevisionHistory;
import com.nedap.archie.rm.generic.RevisionHistoryItem;
import com.nedap.archie.rm.support.identification.ObjectVersionId;
import com.nedap.archie.rm.support.identification.UIDBasedId;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import javax.annotation.Nullable;
import org.ehrbase.api.dto.experimental.ItemTagDto.ItemTagRMType;
import org.ehrbase.api.exception.BadGatewayException;
import org.ehrbase.api.exception.InternalServerException;
import org.ehrbase.api.exception.InvalidApiParameterException;
import org.ehrbase.api.exception.ObjectNotFoundException;
import org.ehrbase.api.exception.PreconditionFailedException;
import org.ehrbase.api.exception.UnexpectedSwitchCaseException;
import org.ehrbase.api.exception.UnprocessableEntityException;
import org.ehrbase.api.exception.ValidationException;
import org.ehrbase.api.service.CompositionService;
import org.ehrbase.api.service.EhrService;
import org.ehrbase.api.service.SystemService;
import org.ehrbase.api.service.ValidationService;
import org.ehrbase.openehr.sdk.response.dto.ehrscape.CompositionDto;
import org.ehrbase.openehr.sdk.response.dto.ehrscape.CompositionFormat;
import org.ehrbase.openehr.sdk.response.dto.ehrscape.StructuredString;
import org.ehrbase.openehr.sdk.response.dto.ehrscape.StructuredStringFormat;
import org.ehrbase.openehr.sdk.serialisation.flatencoding.FlatFormat;
import org.ehrbase.openehr.sdk.serialisation.flatencoding.FlatJasonProvider;
import org.ehrbase.openehr.sdk.serialisation.jsonencoding.CanonicalJson;
import org.ehrbase.openehr.sdk.serialisation.xmlencoding.CanonicalXML;
import org.ehrbase.openehr.sdk.webtemplate.model.WebTemplate;
import org.ehrbase.openehr.sdk.webtemplate.templateprovider.TemplateProvider;
import org.ehrbase.repository.CompositionRepository;
import org.ehrbase.repository.experimental.ItemTagRepository;
import org.ehrbase.util.UuidGenerator;
import org.openehr.schemas.v1.OPERATIONALTEMPLATE;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

/**
 * {@link CompositionService} implementation.
 */
@Service
public class CompositionServiceImp implements CompositionService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final ValidationService validationService;
    private final KnowledgeCacheServiceImp knowledgeCacheService;
    private final EhrService ehrService;

    private final CompositionRepository compositionRepository;
    private final ItemTagRepository itemTagRepository;

    private final SystemService systemService;

    public CompositionServiceImp(
            KnowledgeCacheServiceImp knowledgeCacheService,
            ValidationService validationService,
            EhrService ehrService,
            SystemService systemService,
            CompositionRepository compositionRepository,
            ItemTagRepository itemTagRepository) {

        this.validationService = validationService;
        this.ehrService = ehrService;
        this.knowledgeCacheService = knowledgeCacheService;
        this.compositionRepository = compositionRepository;
        this.itemTagRepository = itemTagRepository;
        this.systemService = systemService;
    }

    @Override
    public Optional create(UUID ehrId, Composition objData, UUID contribution, UUID audit) {
        UUID compositionId = createInternal(ehrId, objData, contribution, audit);
        return Optional.of(compositionId);
    }

    @Override
    public Optional create(UUID ehrId, Composition objData) {
        UUID compositionId = createInternal(ehrId, objData, null, null);
        return Optional.of(compositionId);
    }

    /**
     * Creation of a new composition. With optional custom contribution, or one will be created.
     *
     * @param ehrId          ID of EHR
     * @param composition    RMObject instance of the given Composition to be created
     * @param contributionId NULL if is not needed, or ID of given custom contribution
     * @param audit
     * @return ID of created composition
     * @throws InternalServerException when creation failed
     */
    private UUID createInternal(UUID ehrId, Composition composition, UUID contributionId, UUID audit) {

        // pre-step: check for existing and modifiable ehr
        ehrService.checkEhrExistsAndIsModifiable(ehrId);

        // pre-step: validate
        try {
            validationService.check(composition);

        } catch (UnprocessableEntityException | ValidationException | BadGatewayException e) {
            throw e; // forward exception
        } catch (org.ehrbase.openehr.sdk.validation.ValidationException e) {
            throw new UnprocessableEntityException(e.getMessage());
        } catch (IllegalArgumentException e) {
            throw new ValidationException(e);
        } catch (Exception e) {
            throw new InternalServerException(e);
        }

        final ObjectVersionId objectVersionId = checkOrConstructObjectVersionId(composition.getUid());

        composition.setUid(objectVersionId);
        // actual creation
        final UUID compositionId = extractUid(objectVersionId);

        compositionRepository.commit(ehrId, composition, contributionId, audit);

        logger.debug("Composition created: id={}", compositionId);

        return compositionId;
    }

    private ObjectVersionId checkOrConstructObjectVersionId(@Nullable UIDBasedId uid) {
        if (uid == null) {
            return buildObjectVersionId(UuidGenerator.randomUUID(), 1, systemService);

        } else if (uid instanceof ObjectVersionId objectVersionId) {

            if (!"1".equals(objectVersionId.getVersionTreeId().getValue())) {
                throw new PreconditionFailedException(
                        "Provided Id %s has a invalid Version. Expect Version 1".formatted(uid));
            }

            if (!Objects.equals(
                    systemService.getSystemId(),
                    objectVersionId.getCreatingSystemId().getValue())) {
                throw new PreconditionFailedException("Mismatch of creating_system_id: %s !=: %s"
                        .formatted(objectVersionId.getCreatingSystemId().getValue(), systemService.getSystemId()));
            }

            if (compositionRepository.exists(
                    UUID.fromString(objectVersionId.getObjectId().getValue()))) {
                throw new PreconditionFailedException("Provided Id %s already exists".formatted(uid));
            }
            return (ObjectVersionId) uid;
        } else {
            throw new PreconditionFailedException("Provided Id %s is not a ObjectVersionId".formatted(uid));
        }
    }

    @Override
    public Optional update(
            UUID ehrId, ObjectVersionId targetObjId, Composition objData, UUID contribution, UUID audit) {

        var compoId = internalUpdate(ehrId, targetObjId, objData, contribution, audit);
        return Optional.of(compoId);
    }

    @Override
    public Optional update(UUID ehrId, ObjectVersionId targetObjId, Composition objData) {
        var compoId = internalUpdate(ehrId, targetObjId, objData, null, null);
        return Optional.of(compoId);
    }

    /**
     * Update of an existing composition. With optional custom contribution, or existing one will be
     * updated.
     *
     * @param compositionId  ID of existing composition
     * @param composition    RMObject instance of the given Composition which represents the new version
     * @param contributionId NULL if new one should be created; or ID of given custom contribution
     * @param audit
     * @return UUID pointing to updated composition
     */
    private UUID internalUpdate(
            UUID ehrId, ObjectVersionId compositionId, Composition composition, UUID contributionId, UUID audit) {

        // pre-step: check for existing and modifiable ehr
        ehrService.checkEhrExistsAndIsModifiable(ehrId);

        // pre-step: validate
        try {
            validationService.check(composition);

        } catch (org.ehrbase.openehr.sdk.validation.ValidationException e) {
            throw new UnprocessableEntityException(e.getMessage());
        } catch (UnprocessableEntityException | ValidationException e) {
            throw e;
        } catch (IllegalArgumentException e) {
            throw new ValidationException(e);
        } catch (Exception e) {
            throw new InternalServerException(e);
        }

        UUID compId = UUID.fromString(compositionId.getObjectId().getValue());
        int version = Integer.parseInt(compositionId.getVersionTreeId().getValue());

        String existingTemplateId = compositionRepository
                .findTemplateId(compId)
                .orElseThrow(() -> new ObjectNotFoundException(
                        "composition", "No COMPOSITION with given id: %s".formatted(compId)));

        String inputTemplateId =
                composition.getArchetypeDetails().getTemplateId().getValue();
        if (!existingTemplateId.equals(inputTemplateId)) {
            // check if base template ID doesn't match  (template ID schema: "$NAME.$LANG.v$VER")
            if (!existingTemplateId.split("\\.")[0].equals(inputTemplateId.split("\\.")[0])) {
                throw new InvalidApiParameterException("Can't update composition to have different template.");
            }
            // if base matches, check if given template ID is just a new version of the correct template
            int existingTemplateIdVersion = Integer.parseInt(existingTemplateId.split("\\.v")[1]);
            int inputTemplateIdVersion =
                    Integer.parseInt(inputTemplateId.substring(inputTemplateId.lastIndexOf("\\.v") + 1));
            if (inputTemplateIdVersion < existingTemplateIdVersion) {
                throw new InvalidApiParameterException("Can't update composition with wrong template version bump.");
            }
        }

        composition.setUid(buildObjectVersionId(compId, version + 1, systemService));

        compositionRepository.update(ehrId, composition, contributionId, audit);

        return compId;
    }

    @Override
    public void delete(UUID ehrId, ObjectVersionId targetObjId, UUID contribution, UUID audit) {
        internalDelete(ehrId, targetObjId, contribution, audit);
    }

    @Override
    public void delete(UUID ehrId, ObjectVersionId targetObjId) {
        internalDelete(ehrId, targetObjId, null, null);
    }

    /**
     * Deletion of an existing composition. With optional custom contribution, or existing one will be
     * updated.
     *
     * @param compositionId  ID of existing composition
     * @param contributionId NULL if is not needed, or ID of given custom contribution
     * @param audit
     */
    private void internalDelete(UUID ehrId, ObjectVersionId compositionId, UUID contributionId, UUID audit) {

        // pre-step: check if ehr exists and is modifiable
        ehrService.checkEhrExistsAndIsModifiable(ehrId);

        compositionRepository.delete(
                ehrId,
                UUID.fromString(compositionId.getObjectId().getValue()),
                extractVersion(compositionId),
                contributionId,
                audit);
    }

    @Override
    public Optional retrieve(UUID ehrId, UUID compositionId, Integer version)
            throws InternalServerException {

        Optional result;

        if (version == null) {
            result = compositionRepository.findHead(ehrId, compositionId);
        } else {
            result = compositionRepository.findByVersion(ehrId, compositionId, version);
        }

        if (result.isEmpty()) {
            // check that the ehr exists and throw error if not
            ehrService.checkEhrExists(ehrId);
        }

        return result;
    }

    @Override
    public Optional getEhrIdForComposition(UUID compositionId) {
        return compositionRepository.findEHRforComposition(compositionId);
    }

    /**
     * Public serializer entry point which will be called with composition dto fetched from database
     * and the desired target serialized string format. Will parse the composition dto into target
     * format either with a custom lambda expression for desired target format
     *
     * @param composition Composition dto from database
     * @param format      Target format
     * @return Structured string with string of data and content format
     */
    @Override
    public StructuredString serialize(CompositionDto composition, CompositionFormat format) {
        final StructuredString compositionString;
        switch (format) {
            case XML:
                compositionString = new StructuredString(
                        new CanonicalXML().marshal(composition.getComposition(), false), StructuredStringFormat.XML);
                break;
            case JSON:
                compositionString = new StructuredString(
                        new CanonicalJson().marshal(composition.getComposition()), StructuredStringFormat.JSON);
                break;
            case FLAT:
                compositionString = new StructuredString(
                        new FlatJasonProvider(createTemplateProvider())
                                .buildFlatJson(FlatFormat.SIM_SDT, composition.getTemplateId())
                                .marshal(composition.getComposition()),
                        StructuredStringFormat.JSON);
                break;
            case STRUCTURED:
                compositionString = new StructuredString(
                        new FlatJasonProvider(createTemplateProvider())
                                .buildFlatJson(FlatFormat.STRUCTURED, composition.getTemplateId())
                                .marshal(composition.getComposition()),
                        StructuredStringFormat.JSON);
                break;
            default:
                throw new UnexpectedSwitchCaseException(format);
        }
        return compositionString;
    }

    public Composition buildComposition(String content, CompositionFormat format, String templateId) {
        final Composition composition;
        switch (format) {
            case XML:
                composition = new CanonicalXML().unmarshal(content, Composition.class);
                break;
            case JSON:
                composition = new CanonicalJson().unmarshal(content, Composition.class);
                break;
            case FLAT:
                composition = new FlatJasonProvider(createTemplateProvider())
                        .buildFlatJson(FlatFormat.SIM_SDT, templateId)
                        .unmarshal(content);
                break;
            case STRUCTURED:
                composition = new FlatJasonProvider(createTemplateProvider())
                        .buildFlatJson(FlatFormat.STRUCTURED, templateId)
                        .unmarshal(content);
                break;
            default:
                throw new UnexpectedSwitchCaseException(format);
        }
        return composition;
    }

    //    private

    private TemplateProvider createTemplateProvider() {
        return new TemplateProvider() {
            @Override
            public Optional find(String s) {
                return knowledgeCacheService.retrieveOperationalTemplate(s);
            }

            @Override
            public Optional buildIntrospect(String templateId) {
                if (templateId == null) {
                    return Optional.empty();
                }
                return Optional.ofNullable(knowledgeCacheService.getWebTemplate(templateId));
            }
        };
    }

    @Override
    public int getLastVersionNumber(UUID compositionId) {

        Optional versionNumber = compositionRepository.getLatestVersionNumber(compositionId);
        return versionNumber.orElseThrow(() -> new ObjectNotFoundException(
                "composition", "No COMPOSITION with given id: %s".formatted(compositionId)));
    }

    @Override
    public int getVersionByTimestamp(UUID compositionId, OffsetDateTime timestamp) {

        Optional versionByTime = compositionRepository.findVersionByTime(compositionId, timestamp);
        return versionByTime.orElseThrow(() -> new ObjectNotFoundException(
                "composition", "No COMPOSITION with given id: %s".formatted(compositionId)));
    }

    @Override
    public String retrieveTemplateId(UUID compositionId) {

        return compositionRepository.findTemplateId(compositionId).orElseThrow();
    }

    @Override
    public boolean exists(UUID versionedObjectId) {
        return compositionRepository.exists(versionedObjectId);
    }

    @Override
    public boolean isDeleted(UUID ehrId, UUID versionedObjectId, Integer version) {

        if (version == null) {
            Optional versionNumber = compositionRepository.getLatestVersionNumber(versionedObjectId);
            if (versionNumber.isEmpty()) {
                return false;
            }
            version = versionNumber.get();
        }

        return compositionRepository.isDeleted(ehrId, versionedObjectId, version);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @Override
    public void adminDelete(UUID compositionId) {

        itemTagRepository.adminDelete(compositionId, ItemTagRMType.COMPOSITION);
        compositionRepository.adminDelete(compositionId);
    }

    @Override
    public VersionedComposition getVersionedComposition(UUID ehrId, UUID composition) {

        ehrService.checkEhrExists(ehrId);

        Optional versionedComposition =
                compositionRepository.getVersionedComposition(ehrId, composition);

        if (versionedComposition.isEmpty()) {
            throw new ObjectNotFoundException(
                    "versioned_composition", "No VERSIONED_COMPOSITION with given id: " + composition);
        }
        return versionedComposition.get();
    }

    @Override
    public RevisionHistory getRevisionHistoryOfVersionedComposition(UUID ehrUid, UUID composition) {
        // get number of versions
        int versions = getLastVersionNumber(composition);
        // fetch each version and add to revision history
        RevisionHistory revisionHistory = new RevisionHistory();
        for (int i = 1; i <= versions; i++) {
            Optional> compoVersion = getOriginalVersionComposition(ehrUid, composition, i);
            compoVersion.ifPresent(compositionOriginalVersion ->
                    revisionHistory.addItem(revisionHistoryItemFromComposition(compositionOriginalVersion)));
        }

        if (revisionHistory.getItems().isEmpty()) {
            throw new InternalServerException("Problem creating RevisionHistory"); // never should be empty; not valid
        }
        return revisionHistory;
    }

    private RevisionHistoryItem revisionHistoryItemFromComposition(OriginalVersion composition) {

        ObjectVersionId objectVersionId = composition.getUid();

        // Note: is List but only has more than one item when there are contributions regarding this
        // object of change type attestation
        List auditDetailsList = new ArrayList<>();
        // retrieving the audits
        auditDetailsList.add(composition.getCommitAudit());

        // add retrieval of attestations, if there are any
        if (composition.getAttestations() != null) {
            for (Attestation a : composition.getAttestations()) {
                AuditDetails newAudit = new AuditDetails(
                        a.getSystemId(), a.getCommitter(), a.getTimeCommitted(), a.getChangeType(), a.getDescription());
                auditDetailsList.add(newAudit);
            }
        }

        return new RevisionHistoryItem(objectVersionId, auditDetailsList);
    }

    @Override
    public Optional> getOriginalVersionComposition(
            UUID ehrUid, UUID versionedObjectUid, int version) {

        return compositionRepository.getOriginalVersionComposition(ehrUid, versionedObjectUid, version);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy