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