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}