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.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.model.PersistentIdToForcedIdMap; 027import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 028import ca.uhn.fhir.jpa.model.config.PartitionSettings; 029import ca.uhn.fhir.jpa.model.dao.JpaPid; 030import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 031import ca.uhn.fhir.rest.param.HistorySearchStyleEnum; 032import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 033import com.google.common.collect.ImmutableListMultimap; 034import com.google.common.collect.Multimaps; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037import org.springframework.beans.factory.annotation.Autowired; 038 039import java.util.ArrayList; 040import java.util.Date; 041import java.util.List; 042import java.util.Optional; 043import java.util.Set; 044import java.util.stream.Collectors; 045import javax.annotation.Nullable; 046import javax.persistence.EntityManager; 047import javax.persistence.PersistenceContext; 048import javax.persistence.PersistenceContextType; 049import javax.persistence.TypedQuery; 050import javax.persistence.criteria.CriteriaBuilder; 051import javax.persistence.criteria.CriteriaQuery; 052import javax.persistence.criteria.Expression; 053import javax.persistence.criteria.JoinType; 054import javax.persistence.criteria.Predicate; 055import javax.persistence.criteria.Root; 056import javax.persistence.criteria.Subquery; 057 058import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toPredicateArray; 059 060/** 061 * The HistoryBuilder is responsible for building history queries 062 */ 063public class HistoryBuilder { 064 065 private static final Logger ourLog = LoggerFactory.getLogger(HistoryBuilder.class); 066 private final String myResourceType; 067 private final Long myResourceId; 068 private final Date myRangeStartInclusive; 069 private final Date myRangeEndInclusive; 070 071 @Autowired 072 protected IInterceptorBroadcaster myInterceptorBroadcaster; 073 074 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 075 protected EntityManager myEntityManager; 076 077 @Autowired 078 private PartitionSettings myPartitionSettings; 079 080 @Autowired 081 private FhirContext myCtx; 082 083 @Autowired 084 private IIdHelperService myIdHelperService; 085 086 /** 087 * Constructor 088 */ 089 public HistoryBuilder( 090 @Nullable String theResourceType, 091 @Nullable Long theResourceId, 092 @Nullable Date theRangeStartInclusive, 093 @Nullable Date theRangeEndInclusive) { 094 myResourceType = theResourceType; 095 myResourceId = theResourceId; 096 myRangeStartInclusive = theRangeStartInclusive; 097 myRangeEndInclusive = theRangeEndInclusive; 098 } 099 100 public Long fetchCount(RequestPartitionId thePartitionId) { 101 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 102 CriteriaQuery<Long> criteriaQuery = cb.createQuery(Long.class); 103 Root<ResourceHistoryTable> from = criteriaQuery.from(ResourceHistoryTable.class); 104 criteriaQuery.select(cb.count(from)); 105 106 addPredicatesToQuery(cb, thePartitionId, criteriaQuery, from, null); 107 108 TypedQuery<Long> query = myEntityManager.createQuery(criteriaQuery); 109 return query.getSingleResult(); 110 } 111 112 @SuppressWarnings("OptionalIsPresent") 113 public List<ResourceHistoryTable> fetchEntities( 114 RequestPartitionId thePartitionId, 115 Integer theOffset, 116 int theFromIndex, 117 int theToIndex, 118 HistorySearchStyleEnum theHistorySearchStyle) { 119 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 120 CriteriaQuery<ResourceHistoryTable> criteriaQuery = cb.createQuery(ResourceHistoryTable.class); 121 Root<ResourceHistoryTable> from = criteriaQuery.from(ResourceHistoryTable.class); 122 123 addPredicatesToQuery(cb, thePartitionId, criteriaQuery, from, theHistorySearchStyle); 124 125 from.fetch("myProvenance", JoinType.LEFT); 126 127 criteriaQuery.orderBy(cb.desc(from.get("myUpdated"))); 128 129 TypedQuery<ResourceHistoryTable> query = myEntityManager.createQuery(criteriaQuery); 130 131 int startIndex = theFromIndex; 132 if (theOffset != null) { 133 startIndex += theOffset; 134 } 135 query.setFirstResult(startIndex); 136 137 query.setMaxResults(theToIndex - theFromIndex); 138 139 List<ResourceHistoryTable> tables = query.getResultList(); 140 if (tables.size() > 0) { 141 ImmutableListMultimap<Long, ResourceHistoryTable> resourceIdToHistoryEntries = 142 Multimaps.index(tables, ResourceHistoryTable::getResourceId); 143 Set<JpaPid> pids = resourceIdToHistoryEntries.keySet().stream() 144 .map(JpaPid::fromId) 145 .collect(Collectors.toSet()); 146 PersistentIdToForcedIdMap pidToForcedId = myIdHelperService.translatePidsToForcedIds(pids); 147 ourLog.trace("Translated IDs: {}", pidToForcedId.getResourcePersistentIdOptionalMap()); 148 149 for (Long nextResourceId : resourceIdToHistoryEntries.keySet()) { 150 List<ResourceHistoryTable> historyTables = resourceIdToHistoryEntries.get(nextResourceId); 151 152 String resourceId; 153 154 Optional<String> forcedId = pidToForcedId.get(JpaPid.fromId(nextResourceId)); 155 if (forcedId.isPresent()) { 156 resourceId = forcedId.get(); 157 } else { 158 resourceId = nextResourceId.toString(); 159 } 160 161 for (ResourceHistoryTable nextHistoryTable : historyTables) { 162 nextHistoryTable.setTransientForcedId(resourceId); 163 } 164 } 165 } 166 167 return tables; 168 } 169 170 private void addPredicatesToQuery( 171 CriteriaBuilder theCriteriaBuilder, 172 RequestPartitionId thePartitionId, 173 CriteriaQuery<?> theQuery, 174 Root<ResourceHistoryTable> theFrom, 175 HistorySearchStyleEnum theHistorySearchStyle) { 176 List<Predicate> predicates = new ArrayList<>(); 177 178 if (!thePartitionId.isAllPartitions()) { 179 if (thePartitionId.isDefaultPartition()) { 180 predicates.add(theCriteriaBuilder.isNull( 181 theFrom.get("myPartitionIdValue").as(Integer.class))); 182 } else if (thePartitionId.hasDefaultPartitionId()) { 183 predicates.add(theCriteriaBuilder.or( 184 theCriteriaBuilder.isNull( 185 theFrom.get("myPartitionIdValue").as(Integer.class)), 186 theFrom.get("myPartitionIdValue") 187 .as(Integer.class) 188 .in(thePartitionId.getPartitionIdsWithoutDefault()))); 189 } else { 190 predicates.add( 191 theFrom.get("myPartitionIdValue").as(Integer.class).in(thePartitionId.getPartitionIds())); 192 } 193 } 194 195 if (myResourceId != null) { 196 predicates.add(theCriteriaBuilder.equal(theFrom.get("myResourceId"), myResourceId)); 197 } else if (myResourceType != null) { 198 validateNotSearchingAllPartitions(thePartitionId); 199 predicates.add(theCriteriaBuilder.equal(theFrom.get("myResourceType"), myResourceType)); 200 } else { 201 validateNotSearchingAllPartitions(thePartitionId); 202 } 203 204 if (myRangeStartInclusive != null) { 205 if (HistorySearchStyleEnum.AT == theHistorySearchStyle && myResourceId != null) { 206 addPredicateForAtQueryParameter(theCriteriaBuilder, theQuery, theFrom, predicates); 207 } else { 208 predicates.add(theCriteriaBuilder.greaterThanOrEqualTo( 209 theFrom.get("myUpdated").as(Date.class), myRangeStartInclusive)); 210 } 211 } 212 if (myRangeEndInclusive != null) { 213 predicates.add(theCriteriaBuilder.lessThanOrEqualTo( 214 theFrom.get("myUpdated").as(Date.class), myRangeEndInclusive)); 215 } 216 217 if (predicates.size() > 0) { 218 theQuery.where(toPredicateArray(predicates)); 219 } 220 } 221 222 private void addPredicateForAtQueryParameter( 223 CriteriaBuilder theCriteriaBuilder, 224 CriteriaQuery<?> theQuery, 225 Root<ResourceHistoryTable> theFrom, 226 List<Predicate> thePredicates) { 227 Subquery<Date> pastDateSubQuery = theQuery.subquery(Date.class); 228 Root<ResourceHistoryTable> subQueryResourceHistory = pastDateSubQuery.from(ResourceHistoryTable.class); 229 Expression<Date> myUpdatedMostRecent = 230 theCriteriaBuilder.max(subQueryResourceHistory.get("myUpdated")).as(Date.class); 231 Expression<Date> myUpdatedMostRecentOrDefault = 232 theCriteriaBuilder.coalesce(myUpdatedMostRecent, theCriteriaBuilder.literal(myRangeStartInclusive)); 233 234 pastDateSubQuery 235 .select(myUpdatedMostRecentOrDefault) 236 .where( 237 theCriteriaBuilder.lessThanOrEqualTo( 238 subQueryResourceHistory.get("myUpdated").as(Date.class), myRangeStartInclusive), 239 theCriteriaBuilder.equal(subQueryResourceHistory.get("myResourceId"), myResourceId)); 240 241 Predicate updatedDatePredicate = 242 theCriteriaBuilder.greaterThanOrEqualTo(theFrom.get("myUpdated").as(Date.class), pastDateSubQuery); 243 thePredicates.add(updatedDatePredicate); 244 } 245 246 private void validateNotSearchingAllPartitions(RequestPartitionId thePartitionId) { 247 if (myPartitionSettings.isPartitioningEnabled()) { 248 if (thePartitionId.isAllPartitions()) { 249 String msg = myCtx.getLocalizer() 250 .getMessage(HistoryBuilder.class, "noSystemOrTypeHistoryForPartitionAwareServer"); 251 throw new InvalidRequestException(Msg.code(953) + msg); 252 } 253 } 254 } 255}