001/*-
002 * #%L
003 * HAPI FHIR Storage api
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.util;
021
022import jakarta.annotation.Nonnull;
023import org.apache.commons.lang3.Validate;
024import org.apache.commons.text.StringTokenizer;
025
026/**
027 * This class parses and serializes the <code>X-Transaction-Semantics</code>
028 * header, which is a custom HAPI FHIR extension affecting the way that
029 * FHIR transactions are processed.
030 *
031 * @see ca.uhn.fhir.jpa.dao.BaseTransactionProcessor
032 * @since 8.2.0
033 */
034public class TransactionSemanticsHeader {
035
036        public static final String RETRY_COUNT = "retryCount";
037        public static final String MIN_DELAY = "minRetryDelay";
038        public static final String MAX_DELAY = "maxRetryDelay";
039        public static final String TRY_BATCH_AS_TRANSACTION_FIRST = "tryBatchAsTransactionFirst";
040        public static final TransactionSemanticsHeader DEFAULT = newBuilder().build();
041        public static final String HEADER_NAME = "X-Transaction-Semantics";
042
043        private final Integer myRetryCount;
044        private final Integer myMinRetryDelay;
045        private final Integer myMaxRetryDelay;
046        private final boolean myTryBatchAsTransactionFirst;
047
048        /**
049         * Non-instantiable, see {@link #newBuilder()}
050         */
051        private TransactionSemanticsHeader(
052                        Integer theRetryCount,
053                        Integer theMinRetryDelay,
054                        Integer theMaxRetryDelay,
055                        boolean theTryAsBatchAsTransactionFirst) {
056                myRetryCount = theRetryCount;
057                myMinRetryDelay = theMinRetryDelay;
058                myMaxRetryDelay = theMaxRetryDelay;
059                myTryBatchAsTransactionFirst = theTryAsBatchAsTransactionFirst;
060        }
061
062        /**
063         * Specifies the number of retry attempts which should be attempted
064         * if the initial transaction processing fails with any kind of error.
065         * A value of 0 (or {@literal null}) means that the transaction will be
066         * attempted only once (i.e. the default behaviour). A value of 2 means
067         * that the transaction will be attempted once, and if it fails, up to
068         * two more attempts will be made before giving up.
069         */
070        public Integer getRetryCount() {
071                return myRetryCount;
072        }
073
074        /**
075         * When automatically retrying a failed transaction, the system will
076         * first sleep for a minimum of this number of milliseconds.
077         */
078        public Integer getMinRetryDelay() {
079                return myMinRetryDelay;
080        }
081
082        /**
083         * When automatically retrying a failed transaction, the system will
084         * first sleep for a minimum of this number of milliseconds.
085         */
086        public Integer getMaxRetryDelay() {
087                return myMaxRetryDelay;
088        }
089
090        /**
091         * When processing a FHIR Batch bundle, try it as a FHIR transaction first, and only switch
092         * to batch mode on the first retry. This option is useful in cases where data can safely be
093         * ingested as a FHIR Batch, since FHIR Transaction processing is generally significantly
094         * faster. However, FHIR transactions exhibit an all-or-nothing failure mode which is not
095         * always desirable for batch ingestion, so this option provides an easy fallback which brings
096         * the benefits of both approaches.
097         */
098        public boolean isTryBatchAsTransactionFirst() {
099                return myTryBatchAsTransactionFirst;
100        }
101
102        /**
103         * Serializes the values as a header value (not including the header name)
104         */
105        public String toHeaderValue() {
106                StringBuilder b = new StringBuilder();
107
108                if (myRetryCount != null) {
109                        b.append(RETRY_COUNT).append('=').append(myRetryCount);
110
111                        // None of the following settings has any meaning unless a
112                        // retry count is specified
113                        if (myMinRetryDelay != null) {
114                                b.append("; ");
115                                b.append(MIN_DELAY).append('=').append(myMinRetryDelay);
116                        }
117                        if (myMaxRetryDelay != null) {
118                                b.append("; ");
119                                b.append(MAX_DELAY).append('=').append(myMaxRetryDelay);
120                        }
121                        if (myTryBatchAsTransactionFirst) {
122                                b.append("; ");
123                                b.append(TRY_BATCH_AS_TRANSACTION_FIRST).append('=').append("true");
124                        }
125                }
126
127                return b.toString();
128        }
129
130        /**
131         * Parses a header value (not including the header name) into a new
132         * {@link TransactionSemanticsHeader} instance.
133         */
134        public static TransactionSemanticsHeader parse(@Nonnull String theHeaderValue) {
135                Validate.notNull(theHeaderValue, "theHeaderValue must not be null");
136                Integer retryCount = null;
137                Integer minRetryDelay = null;
138                Integer maxRetryDelay = null;
139                boolean tryBatchAsTransactionFirst = false;
140
141                StringTokenizer tok = new StringTokenizer(theHeaderValue, ";");
142                while (tok.hasNext()) {
143                        String next = tok.nextToken();
144                        int equalsIdx = next.indexOf('=');
145                        if (equalsIdx == -1) {
146                                continue;
147                        }
148
149                        String name = next.substring(0, equalsIdx).trim();
150                        String value = next.substring(equalsIdx + 1).trim();
151
152                        switch (name) {
153                                case RETRY_COUNT:
154                                        retryCount = parsePositiveInteger(value);
155                                        break;
156                                case MIN_DELAY:
157                                        minRetryDelay = parsePositiveInteger(value);
158                                        break;
159                                case MAX_DELAY:
160                                        maxRetryDelay = parsePositiveInteger(value);
161                                        break;
162                                case TRY_BATCH_AS_TRANSACTION_FIRST:
163                                        tryBatchAsTransactionFirst = parseBoolean(value);
164                                        break;
165                        }
166                }
167
168                return new TransactionSemanticsHeader(retryCount, minRetryDelay, maxRetryDelay, tryBatchAsTransactionFirst);
169        }
170
171        /**
172         * Begin building a new {@link TransactionSemanticsHeader} instance
173         */
174        public static Builder newBuilder() {
175                return new Builder();
176        }
177
178        private static Integer parsePositiveInteger(String theValue) {
179                try {
180                        int retVal = Integer.parseInt(theValue);
181                        if (retVal <= 0) {
182                                return null;
183                        }
184                        return retVal;
185                } catch (NumberFormatException e) {
186                        return null;
187                }
188        }
189
190        private static boolean parseBoolean(String theValue) {
191                return "true".equalsIgnoreCase(theValue);
192        }
193
194        public static final class Builder {
195
196                private Integer myRetryCount;
197                private Integer myMinRetryDelay;
198                private Integer myMaxRetryDelay;
199                private boolean myTryBatchAsTransactionFirst;
200
201                private Builder() {}
202
203                /**
204                 * Specifies the number of retry attempts which should be attempted
205                 * if the initial transaction processing fails with any kind of error.
206                 * A value of 0 (or {@literal null} means that the transaction will be
207                 * attempted once only (i.e. the default behaviour). A value of 2 means
208                 * that the transaction will be attempted once, and if it fails, up to
209                 * two more attempts will be made before giving up.
210                 */
211                public Builder withRetryCount(Integer theRetryCount) {
212                        Validate.isTrue(
213                                        theRetryCount == null || theRetryCount >= 0, "Retry count must be null or a non-negative integer");
214                        myRetryCount = theRetryCount;
215                        return this;
216                }
217
218                /**
219                 * When automatically retrying a failed transaction, the system will
220                 * first sleep for a minimum of this number of milliseconds.
221                 */
222                public Builder withMinRetryDelay(Integer theMinRetryDelay) {
223                        Validate.isTrue(
224                                        theMinRetryDelay == null || theMinRetryDelay >= 0,
225                                        "Retry delay must be null or a non-negative integer");
226                        myMinRetryDelay = theMinRetryDelay;
227                        return this;
228                }
229
230                /**
231                 * When automatically retrying a failed transaction, the system will
232                 * first sleep for a minimum of this number of milliseconds.
233                 */
234                public Builder withMaxRetryDelay(Integer theMaxRetryDelay) {
235                        Validate.isTrue(
236                                        theMaxRetryDelay == null || theMaxRetryDelay >= 0,
237                                        "Retry delay must be null or a non-negative integer");
238                        myMaxRetryDelay = theMaxRetryDelay;
239                        return this;
240                }
241
242                /**
243                 * When processing a FHIR Batch bundle, try it as a FHIR transaction first, and only switch
244                 * to batch mode on the first retry. This option is useful in cases where data can safely be
245                 * ingested as a FHIR Batch, since FHIR Transaction processing is generally significantly
246                 * faster. However, FHIR transactions exhibit an all-or-nothing failure mode which is not
247                 * always desirable for batch ingestion, so this option provides an easy fallback which brings
248                 * the benefits of both approaches.
249                 */
250                public Builder withTryBatchAsTransactionFirst(boolean theTryBatchAsTransactionFirst) {
251                        myTryBatchAsTransactionFirst = theTryBatchAsTransactionFirst;
252                        return this;
253                }
254
255                /**
256                 * Construct the header
257                 */
258                public TransactionSemanticsHeader build() {
259                        return new TransactionSemanticsHeader(
260                                        myRetryCount, myMinRetryDelay, myMaxRetryDelay, myTryBatchAsTransactionFirst);
261                }
262        }
263}