View Javadoc
1   package ca.uhn.fhir.jpa.search;
2   
3   /*
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2018 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   * http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import ca.uhn.fhir.context.FhirContext;
24  import ca.uhn.fhir.jpa.dao.IDao;
25  import ca.uhn.fhir.jpa.dao.ISearchBuilder;
26  import ca.uhn.fhir.jpa.dao.data.ISearchDao;
27  import ca.uhn.fhir.jpa.entity.BaseHasResource;
28  import ca.uhn.fhir.jpa.entity.ResourceHistoryTable;
29  import ca.uhn.fhir.jpa.entity.Search;
30  import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
31  import ca.uhn.fhir.model.primitive.InstantDt;
32  import ca.uhn.fhir.rest.api.server.IBundleProvider;
33  import org.hl7.fhir.instance.model.api.IBaseResource;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  import org.springframework.transaction.PlatformTransactionManager;
37  import org.springframework.transaction.TransactionDefinition;
38  import org.springframework.transaction.TransactionStatus;
39  import org.springframework.transaction.support.TransactionCallback;
40  import org.springframework.transaction.support.TransactionCallbackWithoutResult;
41  import org.springframework.transaction.support.TransactionTemplate;
42  
43  import javax.persistence.EntityManager;
44  import javax.persistence.NoResultException;
45  import javax.persistence.TypedQuery;
46  import javax.persistence.criteria.CriteriaBuilder;
47  import javax.persistence.criteria.CriteriaQuery;
48  import javax.persistence.criteria.Predicate;
49  import javax.persistence.criteria.Root;
50  import java.util.*;
51  
52  public class PersistedJpaBundleProvider implements IBundleProvider {
53  
54  	private static final Logger ourLog = LoggerFactory.getLogger(PersistedJpaBundleProvider.class);
55  	private FhirContext myContext;
56  	private IDao myDao;
57  	private EntityManager myEntityManager;
58  	private PlatformTransactionManager myPlatformTransactionManager;
59  	private ISearchCoordinatorSvc mySearchCoordinatorSvc;
60  	private ISearchDao mySearchDao;
61  	private Search mySearchEntity;
62  	private String myUuid;
63  	private boolean myCacheHit;
64  
65  	public PersistedJpaBundleProvider(String theSearchUuid, IDao theDao) {
66  		myUuid = theSearchUuid;
67  		myDao = theDao;
68  	}
69  
70  	/**
71  	 * When HAPI FHIR server is running "for real", a new
72  	 * instance of the bundle provider is created to serve
73  	 * every HTTP request, so it's ok for us to keep
74  	 * state in here and expect that it will go away. But
75  	 * in unit tests we keep this object around for longer
76  	 * sometimes.
77  	 */
78  	public void clearCachedDataForUnitTest() {
79  		mySearchEntity = null;
80  	}
81  
82  	private List<IBaseResource> doHistoryInTransaction(int theFromIndex, int theToIndex) {
83  		List<ResourceHistoryTable> results;
84  
85  		CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
86  		CriteriaQuery<ResourceHistoryTable> q = cb.createQuery(ResourceHistoryTable.class);
87  		Root<ResourceHistoryTable> from = q.from(ResourceHistoryTable.class);
88  		List<Predicate> predicates = new ArrayList<>();
89  
90  		if (mySearchEntity.getResourceType() == null) {
91  			// All resource types
92  		} else if (mySearchEntity.getResourceId() == null) {
93  			predicates.add(cb.equal(from.get("myResourceType"), mySearchEntity.getResourceType()));
94  		} else {
95  			predicates.add(cb.equal(from.get("myResourceId"), mySearchEntity.getResourceId()));
96  		}
97  
98  		if (mySearchEntity.getLastUpdatedLow() != null) {
99  			predicates.add(cb.greaterThanOrEqualTo(from.get("myUpdated").as(Date.class), mySearchEntity.getLastUpdatedLow()));
100 		}
101 		if (mySearchEntity.getLastUpdatedHigh() != null) {
102 			predicates.add(cb.lessThanOrEqualTo(from.get("myUpdated").as(Date.class), mySearchEntity.getLastUpdatedHigh()));
103 		}
104 
105 		if (predicates.size() > 0) {
106 			q.where(predicates.toArray(new Predicate[predicates.size()]));
107 		}
108 
109 		q.orderBy(cb.desc(from.get("myUpdated")));
110 
111 		TypedQuery<ResourceHistoryTable> query = myEntityManager.createQuery(q);
112 
113 		if (theToIndex - theFromIndex > 0) {
114 			query.setFirstResult(theFromIndex);
115 			query.setMaxResults(theToIndex - theFromIndex);
116 		}
117 
118 		results = query.getResultList();
119 
120 		ArrayList<IBaseResource> retVal = new ArrayList<>();
121 		for (ResourceHistoryTable next : results) {
122 			BaseHasResource resource;
123 			resource = next;
124 
125 			retVal.add(myDao.toResource(resource, true));
126 		}
127 
128 		return retVal;
129 	}
130 
131 	protected List<IBaseResource> doSearchOrEverything(final int theFromIndex, final int theToIndex) {
132 		final ISearchBuilder sb = myDao.newSearchBuilder();
133 
134 		String resourceName = mySearchEntity.getResourceType();
135 		Class<? extends IBaseResource> resourceType = myContext.getResourceDefinition(resourceName).getImplementingClass();
136 		sb.setType(resourceType, resourceName);
137 
138 		final List<Long> pidsSubList = mySearchCoordinatorSvc.getResources(myUuid, theFromIndex, theToIndex);
139 
140 		TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager);
141 		template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
142 		return template.execute(theStatus -> toResourceList(sb, pidsSubList));
143 	}
144 
145 	private void ensureDependenciesInjected() {
146 		if (myPlatformTransactionManager == null) {
147 			myDao.injectDependenciesIntoBundleProvider(this);
148 		}
149 	}
150 
151 	/**
152 	 * Returns false if the entity can't be found
153 	 */
154 	public boolean ensureSearchEntityLoaded() {
155 		if (mySearchEntity == null) {
156 			ensureDependenciesInjected();
157 
158 			TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
159 			txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
160 			txTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
161 			return txTemplate.execute(s -> {
162 				try {
163 					setSearchEntity(mySearchDao.findByUuid(myUuid));
164 
165 					if (mySearchEntity == null) {
166 						return false;
167 					}
168 
169 					ourLog.trace("Retrieved search with version {} and total {}", mySearchEntity.getVersion(), mySearchEntity.getTotalCount());
170 
171 					// Load the includes now so that they are available outside of this transaction
172 					mySearchEntity.getIncludes().size();
173 
174 					return true;
175 				} catch (NoResultException e) {
176 					return false;
177 				}
178 			});
179 		}
180 		return true;
181 	}
182 
183 	@Override
184 	public InstantDt getPublished() {
185 		ensureSearchEntityLoaded();
186 		return new InstantDt(mySearchEntity.getCreated());
187 	}
188 
189 	@Override
190 	public List<IBaseResource> getResources(final int theFromIndex, final int theToIndex) {
191 		ensureDependenciesInjected();
192 
193 		TransactionTemplate template = new TransactionTemplate(myPlatformTransactionManager);
194 
195 		template.execute(new TransactionCallbackWithoutResult() {
196 			@Override
197 			protected void doInTransactionWithoutResult(TransactionStatus theStatus) {
198 				ensureSearchEntityLoaded();
199 			}
200 		});
201 
202 		switch (mySearchEntity.getSearchType()) {
203 			case HISTORY:
204 				return template.execute(new TransactionCallback<List<IBaseResource>>() {
205 					@Override
206 					public List<IBaseResource> doInTransaction(TransactionStatus theStatus) {
207 						return doHistoryInTransaction(theFromIndex, theToIndex);
208 					}
209 				});
210 			case SEARCH:
211 			case EVERYTHING:
212 			default:
213 				return doSearchOrEverything(theFromIndex, theToIndex);
214 		}
215 	}
216 
217 	@Override
218 	public String getUuid() {
219 		return myUuid;
220 	}
221 
222 	public boolean isCacheHit() {
223 		return myCacheHit;
224 	}
225 
226 	void setCacheHit(boolean theCacheHit) {
227 		myCacheHit = theCacheHit;
228 	}
229 
230 	@Override
231 	public Integer preferredPageSize() {
232 		ensureSearchEntityLoaded();
233 		return mySearchEntity.getPreferredPageSize();
234 	}
235 
236 	public void setContext(FhirContext theContext) {
237 		myContext = theContext;
238 	}
239 
240 	public void setEntityManager(EntityManager theEntityManager) {
241 		myEntityManager = theEntityManager;
242 	}
243 
244 	public void setPlatformTransactionManager(PlatformTransactionManager thePlatformTransactionManager) {
245 		myPlatformTransactionManager = thePlatformTransactionManager;
246 	}
247 
248 	public void setSearchCoordinatorSvc(ISearchCoordinatorSvc theSearchCoordinatorSvc) {
249 		mySearchCoordinatorSvc = theSearchCoordinatorSvc;
250 	}
251 
252 	public void setSearchDao(ISearchDao theSearchDao) {
253 		mySearchDao = theSearchDao;
254 	}
255 
256 	// Note: Leave as protected, HSPC depends on this
257 	@SuppressWarnings("WeakerAccess")
258 	protected void setSearchEntity(Search theSearchEntity) {
259 		mySearchEntity = theSearchEntity;
260 	}
261 
262 	@Override
263 	public Integer size() {
264 		ensureSearchEntityLoaded();
265 		SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearchEntity);
266 
267 		Integer size = mySearchEntity.getTotalCount();
268 		if (size == null) {
269 			return null;
270 		}
271 		return Math.max(0, size);
272 	}
273 
274 	// Note: Leave as protected, HSPC depends on this
275 	@SuppressWarnings("WeakerAccess")
276 	protected List<IBaseResource> toResourceList(ISearchBuilder sb, List<Long> pidsSubList) {
277 		Set<Long> includedPids = new HashSet<>();
278 		if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) {
279 			includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid));
280 			includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid));
281 		}
282 
283 		// Execute the query and make sure we return distinct results
284 		List<IBaseResource> resources = new ArrayList<>();
285 		sb.loadResourcesByPid(pidsSubList, resources, includedPids, false, myEntityManager, myContext, myDao);
286 
287 		return resources;
288 	}
289 
290 }