View Javadoc
1   package ca.uhn.fhir.jpa.config;
2   
3   /*-
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2019 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.i18n.HapiLocalizer;
24  import ca.uhn.fhir.jpa.model.entity.ForcedId;
25  import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
26  import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique;
27  import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
28  import org.hibernate.HibernateException;
29  import org.hibernate.StaleStateException;
30  import org.hibernate.exception.ConstraintViolationException;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  import org.springframework.dao.DataAccessException;
34  import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
35  
36  import javax.persistence.PersistenceException;
37  
38  import static org.apache.commons.lang3.StringUtils.isNotBlank;
39  
40  public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect {
41  
42  	private static final Logger ourLog = LoggerFactory.getLogger(HapiFhirHibernateJpaDialect.class);
43  	private HapiLocalizer myLocalizer;
44  
45  	/**
46  	 * Constructor
47  	 */
48  	public HapiFhirHibernateJpaDialect(HapiLocalizer theLocalizer) {
49  		myLocalizer = theLocalizer;
50  	}
51  
52  
53  	public RuntimeException translate(PersistenceException theException, String theMessageToPrepend) {
54  		if (theException.getCause() instanceof HibernateException) {
55  			return new PersistenceException(convertHibernateAccessException((HibernateException) theException.getCause(), theMessageToPrepend));
56  		}
57  		return theException;
58  	}
59  
60  	@Override
61  	protected DataAccessException convertHibernateAccessException(HibernateException theException) {
62  		return convertHibernateAccessException(theException, null);
63  	}
64  
65  	private DataAccessException convertHibernateAccessException(HibernateException theException, String theMessageToPrepend) {
66  		String messageToPrepend = "";
67  		if (isNotBlank(theMessageToPrepend)) {
68  			messageToPrepend = theMessageToPrepend + " - ";
69  		}
70  
71  		if (theException instanceof ConstraintViolationException) {
72  			String constraintName = ((ConstraintViolationException) theException).getConstraintName();
73  			if (isNotBlank(constraintName)) {
74  				if (constraintName.contains(ResourceHistoryTable.IDX_RESVER_ID_VER)) {
75  					throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure"));
76  				}
77  				if (constraintName.contains(ResourceIndexedCompositeStringUnique.IDX_IDXCMPSTRUNIQ_STRING)) {
78  					throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceIndexedCompositeStringUniqueConstraintFailure"));
79  				}
80  				if (constraintName.contains(ForcedId.IDX_FORCEDID_TYPE_FID)) {
81  					throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "forcedIdConstraintFailure"));
82  				}
83  			}
84  		}
85  
86  		/*
87  		 * It would be nice if we could be more precise here, since technically any optimistic lock
88  		 * failure could result in a StaleStateException, but with the error message we're returning
89  		 * we're basically assuming it's an optimistic lock failure on HFJ_RESOURCE.
90  		 *
91  		 * That said, I think this is an OK trade-off. There is a high probability that if this happens
92  		 * it is a failure on HFJ_RESOURCE (there aren't many other tables in our schema that
93  		 * use @Version at all) and this error message is infinitely more comprehensible
94  		 * than the one we'd otherwise return.
95  		 *
96  		 * The actual StaleStateException is thrown in hibernate's Expectations
97  		 * class in a method called "checkBatched" currently. This can all be tested using the
98  		 * StressTestR4Test method testMultiThreadedUpdateSameResourceInTransaction()
99  		 */
100 		if (theException instanceof StaleStateException) {
101 			String msg = messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure");
102 			throw new ResourceVersionConflictException(msg);
103 		}
104 
105 		return super.convertHibernateAccessException(theException);
106 	}
107 
108 }