001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 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.search;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.jpa.dao.data.IResourceSearchUrlDao;
025import ca.uhn.fhir.jpa.model.dao.JpaPid;
026import ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity;
027import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
028import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
029import jakarta.persistence.EntityManager;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032import org.springframework.stereotype.Service;
033import org.springframework.transaction.annotation.Transactional;
034
035import java.util.Date;
036
037/**
038 * This service ensures uniqueness of resources during create or create-on-update
039 * by storing the resource searchUrl.
040 *
041 * @see SearchUrlJobMaintenanceSvcImpl which deletes stale entities
042 */
043@Transactional
044@Service
045public class ResourceSearchUrlSvc {
046        private static final Logger ourLog = LoggerFactory.getLogger(ResourceSearchUrlSvc.class);
047        private final EntityManager myEntityManager;
048
049        private final IResourceSearchUrlDao myResourceSearchUrlDao;
050
051        private final MatchUrlService myMatchUrlService;
052
053        private final FhirContext myFhirContext;
054
055        public ResourceSearchUrlSvc(
056                        EntityManager theEntityManager,
057                        IResourceSearchUrlDao theResourceSearchUrlDao,
058                        MatchUrlService theMatchUrlService,
059                        FhirContext theFhirContext) {
060                myEntityManager = theEntityManager;
061                myResourceSearchUrlDao = theResourceSearchUrlDao;
062                myMatchUrlService = theMatchUrlService;
063                myFhirContext = theFhirContext;
064        }
065
066        /**
067         * Perform removal of entries older than {@code theCutoffDate} since the create operations are done.
068         */
069        public void deleteEntriesOlderThan(Date theCutoffDate) {
070                ourLog.debug("About to delete SearchUrl which are older than {}", theCutoffDate);
071                int deletedCount = myResourceSearchUrlDao.deleteAllWhereCreatedBefore(theCutoffDate);
072                ourLog.debug("Deleted {} SearchUrls", deletedCount);
073        }
074
075        /**
076         * Once a resource is updated or deleted, we can trust that future match checks will find the committed resource in the db.
077         * The use of the constraint table is done, and we can delete it to keep the table small.
078         */
079        public void deleteByResId(long theResId) {
080                myResourceSearchUrlDao.deleteByResId(theResId);
081        }
082
083        /**
084         *  We store a record of match urls with res_id so a db constraint can catch simultaneous creates that slip through.
085         */
086        public void enforceMatchUrlResourceUniqueness(
087                        String theResourceName, String theMatchUrl, JpaPid theResourcePersistentId) {
088                String canonicalizedUrlForStorage = createCanonicalizedUrlForStorage(theResourceName, theMatchUrl);
089
090                ResourceSearchUrlEntity searchUrlEntity =
091                                ResourceSearchUrlEntity.from(canonicalizedUrlForStorage, theResourcePersistentId.getId());
092                // calling dao.save performs a merge operation which implies a trip to
093                // the database to see if the resource exists.  Since we don't need the check, we avoid the trip by calling
094                // em.persist.
095                myEntityManager.persist(searchUrlEntity);
096        }
097
098        /**
099         * Provides a sanitized matchUrl to circumvent ordering matters.
100         */
101        private String createCanonicalizedUrlForStorage(String theResourceName, String theMatchUrl) {
102
103                RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceName);
104                SearchParameterMap matchUrlSearchParameterMap = myMatchUrlService.translateMatchUrl(theMatchUrl, resourceDef);
105
106                String canonicalizedMatchUrl = matchUrlSearchParameterMap.toNormalizedQueryString(myFhirContext);
107
108                return theResourceName + canonicalizedMatchUrl;
109        }
110}