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}