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}