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