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.dao;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.FhirVersionEnum;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
027import ca.uhn.fhir.jpa.api.dao.IDao;
028import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
029import ca.uhn.fhir.jpa.entity.PartitionEntity;
030import ca.uhn.fhir.jpa.entity.ResourceSearchView;
031import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceServiceRegistry;
032import ca.uhn.fhir.jpa.esr.IExternallyStoredResourceService;
033import ca.uhn.fhir.jpa.model.config.PartitionSettings;
034import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
035import ca.uhn.fhir.jpa.model.entity.BaseTag;
036import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity;
037import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
038import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
039import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
040import ca.uhn.fhir.jpa.model.entity.ResourceTable;
041import ca.uhn.fhir.jpa.model.entity.ResourceTag;
042import ca.uhn.fhir.jpa.model.entity.TagDefinition;
043import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
044import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
045import ca.uhn.fhir.model.api.IResource;
046import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
047import ca.uhn.fhir.model.api.Tag;
048import ca.uhn.fhir.model.api.TagList;
049import ca.uhn.fhir.model.base.composite.BaseCodingDt;
050import ca.uhn.fhir.model.primitive.IdDt;
051import ca.uhn.fhir.model.primitive.InstantDt;
052import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
053import ca.uhn.fhir.parser.DataFormatException;
054import ca.uhn.fhir.parser.IParser;
055import ca.uhn.fhir.parser.LenientErrorHandler;
056import ca.uhn.fhir.rest.api.Constants;
057import ca.uhn.fhir.util.MetaUtil;
058import org.apache.commons.lang3.Validate;
059import org.hl7.fhir.instance.model.api.IAnyResource;
060import org.hl7.fhir.instance.model.api.IBaseCoding;
061import org.hl7.fhir.instance.model.api.IBaseMetaType;
062import org.hl7.fhir.instance.model.api.IBaseResource;
063import org.hl7.fhir.instance.model.api.IIdType;
064import org.slf4j.Logger;
065import org.slf4j.LoggerFactory;
066import org.springframework.beans.factory.annotation.Autowired;
067
068import java.util.ArrayList;
069import java.util.Collection;
070import java.util.Collections;
071import java.util.Date;
072import java.util.List;
073import javax.annotation.Nullable;
074
075import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.decodeResource;
076import static java.util.Objects.nonNull;
077import static org.apache.commons.lang3.StringUtils.isNotBlank;
078
079public class JpaStorageResourceParser implements IJpaStorageResourceParser {
080        public static final LenientErrorHandler LENIENT_ERROR_HANDLER = new LenientErrorHandler(false).disableAllErrors();
081        private static final Logger ourLog = LoggerFactory.getLogger(JpaStorageResourceParser.class);
082
083        @Autowired
084        private FhirContext myFhirContext;
085
086        @Autowired
087        private JpaStorageSettings myStorageSettings;
088
089        @Autowired
090        private IResourceHistoryTableDao myResourceHistoryTableDao;
091
092        @Autowired
093        private PartitionSettings myPartitionSettings;
094
095        @Autowired
096        private IPartitionLookupSvc myPartitionLookupSvc;
097
098        @Autowired
099        private ExternallyStoredResourceServiceRegistry myExternallyStoredResourceServiceRegistry;
100
101        @Override
102        public IBaseResource toResource(IBasePersistedResource theEntity, boolean theForHistoryOperation) {
103                RuntimeResourceDefinition type = myFhirContext.getResourceDefinition(theEntity.getResourceType());
104                Class<? extends IBaseResource> resourceType = type.getImplementingClass();
105                return toResource(resourceType, (IBaseResourceEntity) theEntity, null, theForHistoryOperation);
106        }
107
108        @Override
109        public <R extends IBaseResource> R toResource(
110                        Class<R> theResourceType,
111                        IBaseResourceEntity theEntity,
112                        Collection<ResourceTag> theTagList,
113                        boolean theForHistoryOperation) {
114
115                // 1. get resource, it's encoding and the tags if any
116                byte[] resourceBytes;
117                String resourceText;
118                ResourceEncodingEnum resourceEncoding;
119                @Nullable Collection<? extends BaseTag> tagList = Collections.emptyList();
120                long version;
121                String provenanceSourceUri = null;
122                String provenanceRequestId = null;
123
124                if (theEntity instanceof ResourceHistoryTable) {
125                        ResourceHistoryTable history = (ResourceHistoryTable) theEntity;
126                        resourceBytes = history.getResource();
127                        resourceText = history.getResourceTextVc();
128                        resourceEncoding = history.getEncoding();
129                        switch (myStorageSettings.getTagStorageMode()) {
130                                case VERSIONED:
131                                default:
132                                        if (history.isHasTags()) {
133                                                tagList = history.getTags();
134                                        }
135                                        break;
136                                case NON_VERSIONED:
137                                        if (history.getResourceTable().isHasTags()) {
138                                                tagList = history.getResourceTable().getTags();
139                                        }
140                                        break;
141                                case INLINE:
142                                        tagList = null;
143                        }
144                        version = history.getVersion();
145                        if (history.getProvenance() != null) {
146                                provenanceRequestId = history.getProvenance().getRequestId();
147                                provenanceSourceUri = history.getProvenance().getSourceUri();
148                        }
149                } else if (theEntity instanceof ResourceTable) {
150                        ResourceTable resource = (ResourceTable) theEntity;
151                        ResourceHistoryTable history;
152                        if (resource.getCurrentVersionEntity() != null) {
153                                history = resource.getCurrentVersionEntity();
154                        } else {
155                                version = theEntity.getVersion();
156                                history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theEntity.getId(), version);
157                                ((ResourceTable) theEntity).setCurrentVersionEntity(history);
158
159                                while (history == null) {
160                                        if (version > 1L) {
161                                                version--;
162                                                history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(
163                                                                theEntity.getId(), version);
164                                        } else {
165                                                return null;
166                                        }
167                                }
168                        }
169
170                        resourceBytes = history.getResource();
171                        resourceEncoding = history.getEncoding();
172                        resourceText = history.getResourceTextVc();
173                        switch (myStorageSettings.getTagStorageMode()) {
174                                case VERSIONED:
175                                case NON_VERSIONED:
176                                        if (resource.isHasTags()) {
177                                                tagList = resource.getTags();
178                                        }
179                                        break;
180                                case INLINE:
181                                        tagList = null;
182                                        break;
183                        }
184                        version = history.getVersion();
185                        if (history.getProvenance() != null) {
186                                provenanceRequestId = history.getProvenance().getRequestId();
187                                provenanceSourceUri = history.getProvenance().getSourceUri();
188                        }
189                } else if (theEntity instanceof ResourceSearchView) {
190                        // This is the search View
191                        ResourceSearchView view = (ResourceSearchView) theEntity;
192                        resourceBytes = view.getResource();
193                        resourceText = view.getResourceTextVc();
194                        resourceEncoding = view.getEncoding();
195                        version = view.getVersion();
196                        provenanceRequestId = view.getProvenanceRequestId();
197                        provenanceSourceUri = view.getProvenanceSourceUri();
198                        switch (myStorageSettings.getTagStorageMode()) {
199                                case VERSIONED:
200                                case NON_VERSIONED:
201                                        if (theTagList != null) {
202                                                tagList = theTagList;
203                                        }
204                                        break;
205                                case INLINE:
206                                        tagList = null;
207                                        break;
208                        }
209                } else {
210                        // something wrong
211                        return null;
212                }
213
214                // 2. get The text
215                String decodedResourceText = decodedResourceText(resourceBytes, resourceText, resourceEncoding);
216
217                // 3. Use the appropriate custom type if one is specified in the context
218                Class<R> resourceType = determineTypeToParse(theResourceType, tagList);
219
220                // 4. parse the text to FHIR
221                R retVal = parseResource(theEntity, resourceEncoding, decodedResourceText, resourceType);
222
223                // 5. fill MetaData
224                retVal = populateResourceMetadata(theEntity, theForHistoryOperation, tagList, version, retVal);
225
226                // 6. Handle source (provenance)
227                MetaUtil.populateResourceSource(myFhirContext, provenanceSourceUri, provenanceRequestId, retVal);
228
229                // 7. Add partition information
230                populateResourcePartitionInformation(theEntity, retVal);
231
232                return retVal;
233        }
234
235        private <R extends IBaseResource> void populateResourcePartitionInformation(
236                        IBaseResourceEntity theEntity, R retVal) {
237                if (myPartitionSettings.isPartitioningEnabled()) {
238                        PartitionablePartitionId partitionId = theEntity.getPartitionId();
239                        if (partitionId != null && partitionId.getPartitionId() != null) {
240                                PartitionEntity persistedPartition =
241                                                myPartitionLookupSvc.getPartitionById(partitionId.getPartitionId());
242                                retVal.setUserData(Constants.RESOURCE_PARTITION_ID, persistedPartition.toRequestPartitionId());
243                        } else {
244                                retVal.setUserData(Constants.RESOURCE_PARTITION_ID, null);
245                        }
246                }
247        }
248
249        @SuppressWarnings("unchecked")
250        private <R extends IBaseResource> R parseResource(
251                        IBaseResourceEntity theEntity,
252                        ResourceEncodingEnum theResourceEncoding,
253                        String theDecodedResourceText,
254                        Class<R> theResourceType) {
255                R retVal;
256                if (theResourceEncoding == ResourceEncodingEnum.ESR) {
257
258                        int colonIndex = theDecodedResourceText.indexOf(':');
259                        Validate.isTrue(colonIndex > 0, "Invalid ESR address: %s", theDecodedResourceText);
260                        String providerId = theDecodedResourceText.substring(0, colonIndex);
261                        String address = theDecodedResourceText.substring(colonIndex + 1);
262                        Validate.notBlank(providerId, "No provider ID in ESR address: %s", theDecodedResourceText);
263                        Validate.notBlank(address, "No address in ESR address: %s", theDecodedResourceText);
264                        IExternallyStoredResourceService provider =
265                                        myExternallyStoredResourceServiceRegistry.getProvider(providerId);
266                        retVal = (R) provider.fetchResource(address);
267
268                } else if (theResourceEncoding != ResourceEncodingEnum.DEL) {
269
270                        IParser parser = new TolerantJsonParser(
271                                        getContext(theEntity.getFhirVersion()), LENIENT_ERROR_HANDLER, theEntity.getId());
272
273                        try {
274                                retVal = parser.parseResource(theResourceType, theDecodedResourceText);
275                        } catch (Exception e) {
276                                StringBuilder b = new StringBuilder();
277                                b.append("Failed to parse database resource[");
278                                b.append(myFhirContext.getResourceType(theResourceType));
279                                b.append("/");
280                                b.append(theEntity.getIdDt().getIdPart());
281                                b.append(" (pid ");
282                                b.append(theEntity.getId());
283                                b.append(", version ");
284                                b.append(theEntity.getFhirVersion().name());
285                                b.append("): ");
286                                b.append(e.getMessage());
287                                String msg = b.toString();
288                                ourLog.error(msg, e);
289                                throw new DataFormatException(Msg.code(928) + msg, e);
290                        }
291
292                } else {
293
294                        retVal = (R) myFhirContext
295                                        .getResourceDefinition(theEntity.getResourceType())
296                                        .newInstance();
297                }
298                return retVal;
299        }
300
301        @SuppressWarnings("unchecked")
302        private <R extends IBaseResource> Class<R> determineTypeToParse(
303                        Class<R> theResourceType, @Nullable Collection<? extends BaseTag> tagList) {
304                Class<R> resourceType = theResourceType;
305                if (tagList != null) {
306                        if (myFhirContext.hasDefaultTypeForProfile()) {
307                                for (BaseTag nextTag : tagList) {
308                                        if (nextTag.getTag().getTagType() == TagTypeEnum.PROFILE) {
309                                                String profile = nextTag.getTag().getCode();
310                                                if (isNotBlank(profile)) {
311                                                        Class<? extends IBaseResource> newType = myFhirContext.getDefaultTypeForProfile(profile);
312                                                        if (newType != null && theResourceType.isAssignableFrom(newType)) {
313                                                                ourLog.debug("Using custom type {} for profile: {}", newType.getName(), profile);
314                                                                resourceType = (Class<R>) newType;
315                                                                break;
316                                                        }
317                                                }
318                                        }
319                                }
320                        }
321                }
322                return resourceType;
323        }
324
325        @SuppressWarnings("unchecked")
326        @Override
327        public <R extends IBaseResource> R populateResourceMetadata(
328                        IBaseResourceEntity theEntitySource,
329                        boolean theForHistoryOperation,
330                        @Nullable Collection<? extends BaseTag> tagList,
331                        long theVersion,
332                        R theResourceTarget) {
333                if (theResourceTarget instanceof IResource) {
334                        IResource res = (IResource) theResourceTarget;
335                        theResourceTarget =
336                                        (R) populateResourceMetadataHapi(theEntitySource, tagList, theForHistoryOperation, res, theVersion);
337                } else {
338                        IAnyResource res = (IAnyResource) theResourceTarget;
339                        theResourceTarget =
340                                        populateResourceMetadataRi(theEntitySource, tagList, theForHistoryOperation, res, theVersion);
341                }
342                return theResourceTarget;
343        }
344
345        @SuppressWarnings("unchecked")
346        private <R extends IResource> R populateResourceMetadataHapi(
347                        IBaseResourceEntity theEntity,
348                        @Nullable Collection<? extends BaseTag> theTagList,
349                        boolean theForHistoryOperation,
350                        R res,
351                        Long theVersion) {
352                R retVal = res;
353                if (theEntity.getDeleted() != null) {
354                        res = (R) myFhirContext.getResourceDefinition(res).newInstance();
355                        retVal = res;
356                        ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
357                        if (theForHistoryOperation) {
358                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE);
359                        }
360                } else if (theForHistoryOperation) {
361                        /*
362                         * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT.
363                         */
364                        Date published = theEntity.getPublished().getValue();
365                        Date updated = theEntity.getUpdated().getValue();
366                        if (published.equals(updated)) {
367                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST);
368                        } else {
369                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT);
370                        }
371                }
372
373                res.setId(theEntity.getIdDt().withVersion(theVersion.toString()));
374
375                ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion()));
376                ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished());
377                ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated());
378                IDao.RESOURCE_PID.put(res, theEntity.getResourceId());
379
380                if (theTagList != null) {
381                        if (theEntity.isHasTags()) {
382                                TagList tagList = new TagList();
383                                List<IBaseCoding> securityLabels = new ArrayList<>();
384                                List<IdDt> profiles = new ArrayList<>();
385                                for (BaseTag next : theTagList) {
386                                        TagDefinition nextTag = next.getTag();
387                                        switch (nextTag.getTagType()) {
388                                                case PROFILE:
389                                                        profiles.add(new IdDt(nextTag.getCode()));
390                                                        break;
391                                                case SECURITY_LABEL:
392                                                        IBaseCoding secLabel =
393                                                                        (IBaseCoding) myFhirContext.getVersion().newCodingDt();
394                                                        secLabel.setSystem(nextTag.getSystem());
395                                                        secLabel.setCode(nextTag.getCode());
396                                                        secLabel.setDisplay(nextTag.getDisplay());
397                                                        // wipmb these technically support userSelected and version
398                                                        securityLabels.add(secLabel);
399                                                        break;
400                                                case TAG:
401                                                        // wipmb check xml, etc.
402                                                        Tag e = new Tag(nextTag.getSystem(), nextTag.getCode(), nextTag.getDisplay());
403                                                        e.setVersion(nextTag.getVersion());
404                                                        // careful! These are Boolean, not boolean.
405                                                        e.setUserSelectedBoolean(nextTag.getUserSelected());
406                                                        tagList.add(e);
407                                                        break;
408                                        }
409                                }
410                                if (tagList.size() > 0) {
411                                        ResourceMetadataKeyEnum.TAG_LIST.put(res, tagList);
412                                }
413                                if (securityLabels.size() > 0) {
414                                        ResourceMetadataKeyEnum.SECURITY_LABELS.put(res, toBaseCodingList(securityLabels));
415                                }
416                                if (profiles.size() > 0) {
417                                        ResourceMetadataKeyEnum.PROFILES.put(res, profiles);
418                                }
419                        }
420                }
421
422                return retVal;
423        }
424
425        @SuppressWarnings("unchecked")
426        private <R extends IBaseResource> R populateResourceMetadataRi(
427                        IBaseResourceEntity theEntity,
428                        @Nullable Collection<? extends BaseTag> theTagList,
429                        boolean theForHistoryOperation,
430                        IAnyResource res,
431                        Long theVersion) {
432                R retVal = (R) res;
433                if (theEntity.getDeleted() != null) {
434                        res = (IAnyResource) myFhirContext.getResourceDefinition(res).newInstance();
435                        retVal = (R) res;
436                        ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
437                        if (theForHistoryOperation) {
438                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE);
439                        }
440                } else if (theForHistoryOperation) {
441                        /*
442                         * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT.
443                         */
444                        Date published = theEntity.getPublished().getValue();
445                        Date updated = theEntity.getUpdated().getValue();
446                        if (published.equals(updated)) {
447                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST);
448                        } else {
449                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT);
450                        }
451                }
452
453                res.getMeta().setLastUpdated(null);
454                res.getMeta().setVersionId(null);
455
456                updateResourceMetadata(theEntity, res);
457                res.setId(res.getIdElement().withVersion(theVersion.toString()));
458
459                res.getMeta().setLastUpdated(theEntity.getUpdatedDate());
460                IDao.RESOURCE_PID.put(res, theEntity.getResourceId());
461
462                if (theTagList != null) {
463                        res.getMeta().getTag().clear();
464                        res.getMeta().getProfile().clear();
465                        res.getMeta().getSecurity().clear();
466                        for (BaseTag next : theTagList) {
467                                switch (next.getTag().getTagType()) {
468                                        case PROFILE:
469                                                res.getMeta().addProfile(next.getTag().getCode());
470                                                break;
471                                        case SECURITY_LABEL:
472                                                IBaseCoding sec = res.getMeta().addSecurity();
473                                                sec.setSystem(next.getTag().getSystem());
474                                                sec.setCode(next.getTag().getCode());
475                                                sec.setDisplay(next.getTag().getDisplay());
476                                                break;
477                                        case TAG:
478                                                IBaseCoding tag = res.getMeta().addTag();
479                                                tag.setSystem(next.getTag().getSystem());
480                                                tag.setCode(next.getTag().getCode());
481                                                tag.setDisplay(next.getTag().getDisplay());
482                                                tag.setVersion(next.getTag().getVersion());
483                                                Boolean userSelected = next.getTag().getUserSelected();
484                                                // the tag is created with a null userSelected, but the api is primitive boolean.
485                                                // Only update if we are non-null.
486                                                if (nonNull(userSelected)) {
487                                                        tag.setUserSelected(userSelected);
488                                                }
489                                                break;
490                                }
491                        }
492                }
493
494                return retVal;
495        }
496
497        @Override
498        public void updateResourceMetadata(IBaseResourceEntity theEntitySource, IBaseResource theResourceTarget) {
499                IIdType id = theEntitySource.getIdDt();
500                if (myFhirContext.getVersion().getVersion().isRi()) {
501                        id = myFhirContext.getVersion().newIdType().setValue(id.getValue());
502                }
503
504                if (id.hasResourceType() == false) {
505                        id = id.withResourceType(theEntitySource.getResourceType());
506                }
507
508                theResourceTarget.setId(id);
509                if (theResourceTarget instanceof IResource) {
510                        ResourceMetadataKeyEnum.VERSION.put((IResource) theResourceTarget, id.getVersionIdPart());
511                        ResourceMetadataKeyEnum.UPDATED.put((IResource) theResourceTarget, theEntitySource.getUpdated());
512                } else {
513                        IBaseMetaType meta = theResourceTarget.getMeta();
514                        meta.setVersionId(id.getVersionIdPart());
515                        meta.setLastUpdated(theEntitySource.getUpdatedDate());
516                }
517        }
518
519        private FhirContext getContext(FhirVersionEnum theVersion) {
520                Validate.notNull(theVersion, "theVersion must not be null");
521                if (theVersion == myFhirContext.getVersion().getVersion()) {
522                        return myFhirContext;
523                }
524                return FhirContext.forCached(theVersion);
525        }
526
527        private static String decodedResourceText(
528                        byte[] resourceBytes, String resourceText, ResourceEncodingEnum resourceEncoding) {
529                String decodedResourceText;
530                if (resourceText != null) {
531                        decodedResourceText = resourceText;
532                } else {
533                        decodedResourceText = decodeResource(resourceBytes, resourceEncoding);
534                }
535                return decodedResourceText;
536        }
537
538        private static List<BaseCodingDt> toBaseCodingList(List<IBaseCoding> theSecurityLabels) {
539                ArrayList<BaseCodingDt> retVal = new ArrayList<>(theSecurityLabels.size());
540                for (IBaseCoding next : theSecurityLabels) {
541                        retVal.add((BaseCodingDt) next);
542                }
543                return retVal;
544        }
545}