001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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 } 061 return theException; 062 } 063 064 @Override 065 protected DataAccessException convertHibernateAccessException(@Nonnull HibernateException theException) { 066 return convertHibernateAccessException(theException, null); 067 } 068 069 private DataAccessException convertHibernateAccessException( 070 HibernateException theException, String theMessageToPrepend) { 071 String messageToPrepend = ""; 072 if (isNotBlank(theMessageToPrepend)) { 073 messageToPrepend = theMessageToPrepend + " - "; 074 } 075 076 if (HapiSystemProperties.isUnitTestModeEnabled()) { 077 ourLog.error("Unit test mode: Hibernate exception", theException); 078 } 079 080 if (theException instanceof ConstraintViolationException) { 081 String constraintName = ((ConstraintViolationException) theException).getConstraintName(); 082 083 /* 084 * Note: Compare the constraint name in a case-insensitive way. Most DBs preserve the case, but Postgresql 085 * will return it as lowercase even though the definition is in caps. 086 */ 087 if (isNotBlank(constraintName)) { 088 constraintName = constraintName.toUpperCase(); 089 if (constraintName.contains(ResourceHistoryTable.IDX_RESVER_ID_VER)) { 090 throw new ResourceVersionConflictException( 091 Msg.code(823) + makeErrorMessage(messageToPrepend, RESOURCE_VERSION_CONSTRAINT_FAILURE)); 092 } 093 if (constraintName.contains(ResourceIndexedComboStringUnique.IDX_IDXCMPSTRUNIQ_STRING)) { 094 throw new ResourceVersionConflictException(Msg.code(824) 095 + makeErrorMessage( 096 messageToPrepend, "resourceIndexedCompositeStringUniqueConstraintFailure")); 097 } 098 if (constraintName.contains(ResourceTable.IDX_RES_TYPE_FHIR_ID)) { 099 throw new ResourceVersionConflictException( 100 Msg.code(825) + makeErrorMessage(messageToPrepend, "forcedIdConstraintFailure")); 101 } 102 if (constraintName.contains(ResourceSearchUrlEntity.RES_SEARCH_URL_COLUMN_NAME)) { 103 throw super.convertHibernateAccessException(theException); 104 } 105 } 106 } 107 108 /* 109 * It would be nice if we could be more precise here, since technically any optimistic lock 110 * failure could result in a StaleStateException, but with the error message we're returning 111 * we're basically assuming it's an optimistic lock failure on HFJ_RESOURCE. 112 * 113 * That said, I think this is an OK trade-off. There is a high probability that if this happens 114 * it is a failure on HFJ_RESOURCE (there aren't many other tables in our schema that 115 * use @Version at all) and this error message is infinitely more comprehensible 116 * than the one we'd otherwise return. 117 * 118 * The actual StaleStateException is thrown in hibernate's Expectations 119 * class in a method called "checkBatched" currently. This can all be tested using the 120 * StressTestR4Test method testMultiThreadedUpdateSameResourceInTransaction() 121 */ 122 if (theException instanceof org.hibernate.StaleStateException) { 123 throw new ResourceVersionConflictException( 124 Msg.code(826) + makeErrorMessage(messageToPrepend, RESOURCE_VERSION_CONSTRAINT_FAILURE)); 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 throw new ResourceVersionConflictException( 131 Msg.code(827) + makeErrorMessage(messageToPrepend, RESOURCE_VERSION_CONSTRAINT_FAILURE)); 132 } 133 } 134 135 DataAccessException retVal = super.convertHibernateAccessException(theException); 136 return retVal; 137 } 138 139 @Nonnull 140 private String makeErrorMessage(String thePrefix, String theMessageKey) { 141 return thePrefix + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, theMessageKey); 142 } 143}