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.config; 021 022import ca.uhn.fhir.i18n.HapiLocalizer; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.model.entity.ForcedId; 025import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 026import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; 027import ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity; 028import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 029import org.hibernate.HibernateException; 030import org.hibernate.PessimisticLockException; 031import org.hibernate.exception.ConstraintViolationException; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034import org.springframework.dao.DataAccessException; 035import org.springframework.orm.jpa.vendor.HibernateJpaDialect; 036 037import javax.persistence.PersistenceException; 038 039import static org.apache.commons.lang3.StringUtils.defaultString; 040import static org.apache.commons.lang3.StringUtils.isNotBlank; 041 042public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect { 043 044 private static final Logger ourLog = LoggerFactory.getLogger(HapiFhirHibernateJpaDialect.class); 045 private HapiLocalizer myLocalizer; 046 047 /** 048 * Constructor 049 */ 050 public HapiFhirHibernateJpaDialect(HapiLocalizer theLocalizer) { 051 myLocalizer = theLocalizer; 052 } 053 054 public RuntimeException translate(PersistenceException theException, String theMessageToPrepend) { 055 if (theException.getCause() instanceof HibernateException) { 056 return new PersistenceException( 057 convertHibernateAccessException((HibernateException) theException.getCause(), theMessageToPrepend)); 058 } 059 return theException; 060 } 061 062 @Override 063 protected DataAccessException convertHibernateAccessException(HibernateException theException) { 064 return convertHibernateAccessException(theException, null); 065 } 066 067 private DataAccessException convertHibernateAccessException( 068 HibernateException theException, String theMessageToPrepend) { 069 String messageToPrepend = ""; 070 if (isNotBlank(theMessageToPrepend)) { 071 messageToPrepend = theMessageToPrepend + " - "; 072 } 073 074 if (theException instanceof ConstraintViolationException) { 075 String constraintName = ((ConstraintViolationException) theException).getConstraintName(); 076 077 /* 078 * Note: Compare the constraint name in a case-insensitive way. Most DBs preserve the case, but Postgresql 079 * will return it as lowercase even though the definition is in caps. 080 */ 081 if (isNotBlank(constraintName)) { 082 constraintName = constraintName.toUpperCase(); 083 if (constraintName.contains(ResourceHistoryTable.IDX_RESVER_ID_VER)) { 084 throw new ResourceVersionConflictException(Msg.code(823) 085 + messageToPrepend 086 + myLocalizer.getMessage( 087 HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure")); 088 } 089 if (constraintName.contains(ResourceIndexedComboStringUnique.IDX_IDXCMPSTRUNIQ_STRING)) { 090 throw new ResourceVersionConflictException(Msg.code(824) 091 + messageToPrepend 092 + myLocalizer.getMessage( 093 HapiFhirHibernateJpaDialect.class, 094 "resourceIndexedCompositeStringUniqueConstraintFailure")); 095 } 096 if (constraintName.contains(ForcedId.IDX_FORCEDID_TYPE_FID)) { 097 throw new ResourceVersionConflictException(Msg.code(825) 098 + messageToPrepend 099 + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "forcedIdConstraintFailure")); 100 } 101 if (constraintName.contains(ResourceSearchUrlEntity.RES_SEARCH_URL_COLUMN_NAME)) { 102 throw super.convertHibernateAccessException(theException); 103 } 104 } 105 } 106 107 /* 108 * It would be nice if we could be more precise here, since technically any optimistic lock 109 * failure could result in a StaleStateException, but with the error message we're returning 110 * we're basically assuming it's an optimistic lock failure on HFJ_RESOURCE. 111 * 112 * That said, I think this is an OK trade-off. There is a high probability that if this happens 113 * it is a failure on HFJ_RESOURCE (there aren't many other tables in our schema that 114 * use @Version at all) and this error message is infinitely more comprehensible 115 * than the one we'd otherwise return. 116 * 117 * The actual StaleStateException is thrown in hibernate's Expectations 118 * class in a method called "checkBatched" currently. This can all be tested using the 119 * StressTestR4Test method testMultiThreadedUpdateSameResourceInTransaction() 120 */ 121 if (theException instanceof org.hibernate.StaleStateException) { 122 String msg = messageToPrepend 123 + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure"); 124 throw new ResourceVersionConflictException(Msg.code(826) + msg); 125 } 126 if (theException instanceof org.hibernate.PessimisticLockException) { 127 PessimisticLockException ex = (PessimisticLockException) theException; 128 String sql = defaultString(ex.getSQL()).toUpperCase(); 129 if (sql.contains(ResourceHistoryTable.HFJ_RES_VER)) { 130 String msg = messageToPrepend 131 + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure"); 132 throw new ResourceVersionConflictException(Msg.code(827) + msg); 133 } 134 } 135 136 DataAccessException retVal = super.convertHibernateAccessException(theException); 137 return retVal; 138 } 139}