001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.provider;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
025import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
026import ca.uhn.fhir.jpa.patch.FhirPatch;
027import ca.uhn.fhir.model.api.annotation.Description;
028import ca.uhn.fhir.rest.annotation.IdParam;
029import ca.uhn.fhir.rest.annotation.Operation;
030import ca.uhn.fhir.rest.annotation.OperationParam;
031import ca.uhn.fhir.rest.api.server.RequestDetails;
032import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
033import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
034import ca.uhn.fhir.rest.server.provider.ProviderConstants;
035import com.google.common.base.Objects;
036import org.hl7.fhir.instance.model.api.IBaseParameters;
037import org.hl7.fhir.instance.model.api.IBaseResource;
038import org.hl7.fhir.instance.model.api.IIdType;
039import org.hl7.fhir.instance.model.api.IPrimitiveType;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042import org.springframework.beans.factory.annotation.Autowired;
043
044import javax.annotation.Nonnull;
045
046public class DiffProvider {
047        private static final Logger ourLog = LoggerFactory.getLogger(DiffProvider.class);
048
049        @Autowired
050        private FhirContext myContext;
051
052        @Autowired
053        private DaoRegistry myDaoRegistry;
054
055        @Description(
056                        value =
057                                        "This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.",
058                        shortDefinition = "Comparte two resources or two versions of a single resource")
059        @Operation(name = ProviderConstants.DIFF_OPERATION_NAME, global = true, idempotent = true)
060        public IBaseParameters diff(
061                        @IdParam IIdType theResourceId,
062                        @Description(value = "The resource ID and version to diff from", example = "Patient/example/version/1")
063                                        @OperationParam(
064                                                        name = ProviderConstants.DIFF_FROM_VERSION_PARAMETER,
065                                                        typeName = "string",
066                                                        min = 0,
067                                                        max = 1)
068                                        IPrimitiveType<?> theFromVersion,
069                        @Description(
070                                                        value = "Should differences in the Resource.meta element be included in the diff",
071                                                        example = "false")
072                                        @OperationParam(
073                                                        name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER,
074                                                        typeName = "boolean",
075                                                        min = 0,
076                                                        max = 1)
077                                        IPrimitiveType<Boolean> theIncludeMeta,
078                        RequestDetails theRequestDetails) {
079
080                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceId.getResourceType());
081                IBaseResource targetResource = dao.read(theResourceId, theRequestDetails);
082                IBaseResource sourceResource = null;
083
084                Long versionId = targetResource.getIdElement().getVersionIdPartAsLong();
085
086                if (theFromVersion == null || theFromVersion.getValueAsString() == null) {
087
088                        // If no explicit from version is specified, find the next previous existing version
089                        while (--versionId > 0L && sourceResource == null) {
090                                IIdType nextVersionedId = theResourceId.withVersion(Long.toString(versionId));
091                                try {
092                                        sourceResource = dao.read(nextVersionedId, theRequestDetails);
093                                } catch (ResourceNotFoundException e) {
094                                        ourLog.trace("Resource version {} can not be found, most likely it was expunged", nextVersionedId);
095                                }
096                        }
097
098                } else {
099
100                        long fromVersion = Long.parseLong(theFromVersion.getValueAsString());
101                        sourceResource = dao.read(theResourceId.withVersion(Long.toString(fromVersion)), theRequestDetails);
102                }
103
104                FhirPatch fhirPatch = newPatch(theIncludeMeta);
105                return fhirPatch.diff(sourceResource, targetResource);
106        }
107
108        @Description(
109                        "This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.")
110        @Operation(name = ProviderConstants.DIFF_OPERATION_NAME, idempotent = true)
111        public IBaseParameters diff(
112                        @Description(value = "The resource ID and version to diff from", example = "Patient/example/version/1")
113                                        @OperationParam(name = ProviderConstants.DIFF_FROM_PARAMETER, typeName = "id", min = 1, max = 1)
114                                        IIdType theFromVersion,
115                        @Description(value = "The resource ID and version to diff to", example = "Patient/example/version/2")
116                                        @OperationParam(name = ProviderConstants.DIFF_TO_PARAMETER, typeName = "id", min = 1, max = 1)
117                                        IIdType theToVersion,
118                        @Description(
119                                                        value = "Should differences in the Resource.meta element be included in the diff",
120                                                        example = "false")
121                                        @OperationParam(
122                                                        name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER,
123                                                        typeName = "boolean",
124                                                        min = 0,
125                                                        max = 1)
126                                        IPrimitiveType<Boolean> theIncludeMeta,
127                        RequestDetails theRequestDetails) {
128
129                if (!Objects.equal(theFromVersion.getResourceType(), theToVersion.getResourceType())) {
130                        String msg = myContext.getLocalizer().getMessage(DiffProvider.class, "cantDiffDifferentTypes");
131                        throw new InvalidRequestException(Msg.code(1129) + msg);
132                }
133
134                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theFromVersion.getResourceType());
135                IBaseResource sourceResource = dao.read(theFromVersion, theRequestDetails);
136                IBaseResource targetResource = dao.read(theToVersion, theRequestDetails);
137
138                FhirPatch fhirPatch = newPatch(theIncludeMeta);
139                return fhirPatch.diff(sourceResource, targetResource);
140        }
141
142        @Nonnull
143        public FhirPatch newPatch(IPrimitiveType<Boolean> theIncludeMeta) {
144                FhirPatch fhirPatch = new FhirPatch(myContext);
145                fhirPatch.setIncludePreviousValueInDiff(true);
146
147                if (theIncludeMeta != null && theIncludeMeta.getValue()) {
148                        ourLog.trace("Including resource metadata in patch");
149                } else {
150                        fhirPatch.addIgnorePath("*.meta");
151                }
152
153                return fhirPatch;
154        }
155}