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