001/*-
002 * #%L
003 * HAPI FHIR Subscription 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.topic;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.subscription.SubscriptionConstants;
024import ca.uhn.hapi.converters.canonical.SubscriptionTopicCanonicalizer;
025import org.hl7.fhir.r4.model.Basic;
026import org.hl7.fhir.r4.model.BooleanType;
027import org.hl7.fhir.r4.model.CodeableConcept;
028import org.hl7.fhir.r4.model.Coding;
029import org.hl7.fhir.r4.model.DateTimeType;
030import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
031import org.hl7.fhir.r4.model.Extension;
032import org.hl7.fhir.r4.model.MarkdownType;
033import org.hl7.fhir.r4.model.StringType;
034import org.hl7.fhir.r4.model.UriType;
035
036import java.util.Date;
037
038/**
039 * Builder class for creating and configuring FHIR R4 SubscriptionTopic resources.
040 * <p/>
041 * In R4, SubscriptionTopic is represented as a Basic resource with extensions,
042 * following the pattern outlined in the FHIR Subscriptions Backport implementation guide.
043 * This builder provides a fluent API to create these resources without needing to
044 * handle extension management directly.
045 *
046 * @see SubscriptionTopicCanonicalizer For conversion between R4 Basic and R5 SubscriptionTopic
047 */
048public class R4SubscriptionTopicBuilder {
049
050        private final Basic myTopic;
051        private Extension myCurrentResourceTrigger;
052        private Extension myCurrentCanFilterBy;
053        private Extension myCurrentNotificationShape;
054
055        /**
056         * Creates a new builder with a Basic resource having the SubscriptionTopic code
057         */
058        public R4SubscriptionTopicBuilder() {
059                myTopic = new Basic();
060
061                // Set the Basic.code to indicate this is a SubscriptionTopic
062                CodeableConcept code = new CodeableConcept();
063                code.addCoding(new Coding().setSystem("http://hl7.org/fhir/fhir-types").setCode("SubscriptionTopic"));
064                myTopic.setCode(code);
065        }
066
067        /**
068         * Set the logical ID of the SubscriptionTopic
069         */
070        public R4SubscriptionTopicBuilder setId(String theId) {
071                myTopic.setId(theId);
072                return this;
073        }
074
075        /**
076         * Set the canonical URL of the topic
077         */
078        public R4SubscriptionTopicBuilder setUrl(String theUrl) {
079                addExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_TOPIC_URL, new UriType(theUrl));
080                return this;
081        }
082
083        /**
084         * Set the version of the topic
085         */
086        public R4SubscriptionTopicBuilder setVersion(String theVersion) {
087                addExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_TOPIC_VERSION, new StringType(theVersion));
088                return this;
089        }
090
091        /**
092         * Set the name of the topic
093         */
094        public R4SubscriptionTopicBuilder setName(String theName) {
095                addExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_TOPIC_NAME, new StringType(theName));
096                return this;
097        }
098
099        /**
100         * Set the title of the topic
101         */
102        public R4SubscriptionTopicBuilder setTitle(String theTitle) {
103                addExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_TOPIC_TITLE, new StringType(theTitle));
104                return this;
105        }
106
107        /**
108         * Set the date the topic was last updated
109         */
110        public R4SubscriptionTopicBuilder setDate(Date theDate) {
111                addExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_TOPIC_DATE, new DateTimeType(theDate));
112                return this;
113        }
114
115        /**
116         * Set the description of the topic
117         */
118        public R4SubscriptionTopicBuilder setDescription(String theDescription) {
119                addExtension(
120                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_TOPIC_DESCRIPTION, new MarkdownType(theDescription));
121                return this;
122        }
123
124        /**
125         * Set the status of the topic (note: uses modifier extension)
126         */
127        public R4SubscriptionTopicBuilder setStatus(PublicationStatus theStatus) {
128                Extension statusExtension = new Extension()
129                                .setUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_TOPIC_STATUS)
130                                .setValue(new StringType(theStatus.toCode()));
131                myTopic.addModifierExtension(statusExtension);
132                return this;
133        }
134
135        /**
136         * Start defining a resource trigger
137         */
138        public R4SubscriptionTopicBuilder addResourceTrigger() {
139                myCurrentResourceTrigger =
140                                new Extension().setUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_RESOURCE_TRIGGER);
141                myTopic.addExtension(myCurrentResourceTrigger);
142                return this;
143        }
144
145        /**
146         * Set the description for the current resource trigger
147         */
148        public R4SubscriptionTopicBuilder setResourceTriggerDescription(String theDescription) {
149                checkCurrentResourceTrigger();
150                addNestedExtension(
151                                myCurrentResourceTrigger,
152                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_DESCRIPTION,
153                                new MarkdownType(theDescription));
154                return this;
155        }
156
157        /**
158         * Set the resource type for the current resource trigger
159         */
160        public R4SubscriptionTopicBuilder setResourceTriggerResource(String theResourceType) {
161                checkCurrentResourceTrigger();
162                addNestedExtension(
163                                myCurrentResourceTrigger,
164                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_RESOURCE,
165                                new UriType(theResourceType));
166                return this;
167        }
168
169        /**
170         * Add a supported interaction to the current resource trigger
171         */
172        public R4SubscriptionTopicBuilder addResourceTriggerSupportedInteraction(String theInteractionCode) {
173                checkCurrentResourceTrigger();
174                addNestedExtension(
175                                myCurrentResourceTrigger,
176                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_SUPPORTED_INTERACTION,
177                                new StringType(theInteractionCode));
178                return this;
179        }
180
181        /**
182         * Add FHIRPath criteria to the current resource trigger
183         */
184        public R4SubscriptionTopicBuilder setResourceTriggerFhirPathCriteria(String theFhirPathExpression) {
185                checkCurrentResourceTrigger();
186                addNestedExtension(
187                                myCurrentResourceTrigger,
188                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_FHIRPATH_CRITERIA,
189                                new StringType(theFhirPathExpression));
190                return this;
191        }
192
193        /**
194         * Start defining a query criteria extension for the current resource trigger
195         */
196        public R4SubscriptionTopicBuilder addResourceTriggerQueryCriteria() {
197                checkCurrentResourceTrigger();
198                Extension queryCriteria =
199                                new Extension().setUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_QUERY_CRITERIA);
200                myCurrentResourceTrigger.addExtension(queryCriteria);
201                return this;
202        }
203
204        /**
205         * Set the previous query string for the current resource trigger's query criteria
206         */
207        public R4SubscriptionTopicBuilder setResourceTriggerQueryCriteriaPrevious(String thePreviousQuery) {
208                checkCurrentResourceTrigger();
209                Extension queryCriteria = getOrCreateQueryCriteriaExtension();
210                queryCriteria.addExtension(new Extension()
211                                .setUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_QUERY_CRITERIA_PREVIOUS)
212                                .setValue(new StringType(thePreviousQuery)));
213                return this;
214        }
215
216        /**
217         * Set the current query string for the current resource trigger's query criteria
218         */
219        public R4SubscriptionTopicBuilder setResourceTriggerQueryCriteriaCurrent(String theCurrentQuery) {
220                checkCurrentResourceTrigger();
221                Extension queryCriteria = getOrCreateQueryCriteriaExtension();
222                queryCriteria.addExtension(new Extension()
223                                .setUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_QUERY_CRITERIA_CURRENT)
224                                .setValue(new StringType(theCurrentQuery)));
225                return this;
226        }
227
228        /**
229         * Set requireBoth flag for the current resource trigger's query criteria
230         */
231        public R4SubscriptionTopicBuilder setResourceTriggerQueryCriteriaRequireBoth(boolean theRequireBoth) {
232                checkCurrentResourceTrigger();
233                Extension queryCriteria = getOrCreateQueryCriteriaExtension();
234                queryCriteria.addExtension(new Extension()
235                                .setUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_QUERY_CRITERIA_REQUIRE_BOTH)
236                                .setValue(new BooleanType(theRequireBoth)));
237                return this;
238        }
239
240        /**
241         * Start defining a can-filter-by extension
242         */
243        public R4SubscriptionTopicBuilder addCanFilterBy() {
244                myCurrentCanFilterBy = new Extension().setUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_CAN_FILTER_BY);
245                myTopic.addExtension(myCurrentCanFilterBy);
246                return this;
247        }
248
249        /**
250         * Set the description for the current can-filter-by
251         */
252        public R4SubscriptionTopicBuilder setCanFilterByDescription(String theDescription) {
253                checkCurrentCanFilterBy();
254                addNestedExtension(
255                                myCurrentCanFilterBy,
256                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_DESCRIPTION,
257                                new MarkdownType(theDescription));
258                return this;
259        }
260
261        /**
262         * Set the resource type for the current can-filter-by
263         */
264        public R4SubscriptionTopicBuilder setCanFilterByResource(String theResourceType) {
265                checkCurrentCanFilterBy();
266                addNestedExtension(
267                                myCurrentCanFilterBy,
268                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_RESOURCE,
269                                new UriType(theResourceType));
270                return this;
271        }
272
273        /**
274         * Set the filter parameter for the current can-filter-by
275         */
276        public R4SubscriptionTopicBuilder setCanFilterByParameter(String theFilterParameter) {
277                checkCurrentCanFilterBy();
278                addNestedExtension(
279                                myCurrentCanFilterBy,
280                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_FILTER_PARAMETER,
281                                new StringType(theFilterParameter));
282                return this;
283        }
284
285        /**
286         * Start defining a notification shape extension
287         */
288        public R4SubscriptionTopicBuilder addNotificationShape() {
289                myCurrentNotificationShape =
290                                new Extension().setUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_NOTIFICATION_SHAPE);
291                myTopic.addExtension(myCurrentNotificationShape);
292                return this;
293        }
294
295        /**
296         * Set the resource type for the current notification shape
297         */
298        public R4SubscriptionTopicBuilder setNotificationShapeResource(String theResourceType) {
299                checkCurrentNotificationShape();
300                addNestedExtension(
301                                myCurrentNotificationShape,
302                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_RESOURCE,
303                                new UriType(theResourceType));
304                return this;
305        }
306
307        /**
308         * Add an include parameter to the current notification shape
309         */
310        public R4SubscriptionTopicBuilder addNotificationShapeInclude(String theIncludeParam) {
311                checkCurrentNotificationShape();
312                addNestedExtension(
313                                myCurrentNotificationShape,
314                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_INCLUDE,
315                                new StringType(theIncludeParam));
316                return this;
317        }
318
319        /**
320         * Add a revInclude parameter to the current notification shape
321         */
322        public R4SubscriptionTopicBuilder addNotificationShapeRevInclude(String theRevIncludeParam) {
323                checkCurrentNotificationShape();
324                addNestedExtension(
325                                myCurrentNotificationShape,
326                                SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_REVINCLUDE,
327                                new StringType(theRevIncludeParam));
328                return this;
329        }
330
331        /**
332         * Return the built Basic resource representing the SubscriptionTopic
333         */
334        public Basic build() {
335                return myTopic;
336        }
337
338        // Helper methods
339
340        private void checkCurrentResourceTrigger() {
341                if (myCurrentResourceTrigger == null) {
342                        throw new IllegalStateException(
343                                        Msg.code(2668) + "No current resource trigger defined. Call addResourceTrigger() first.");
344                }
345        }
346
347        private void checkCurrentCanFilterBy() {
348                if (myCurrentCanFilterBy == null) {
349                        throw new IllegalStateException(
350                                        Msg.code(2669) + "No current can-filter-by defined. Call addCanFilterBy() first.");
351                }
352        }
353
354        private void checkCurrentNotificationShape() {
355                if (myCurrentNotificationShape == null) {
356                        throw new IllegalStateException(
357                                        Msg.code(2670) + "No current notification shape defined. Call addNotificationShape() first.");
358                }
359        }
360
361        private Extension getOrCreateQueryCriteriaExtension() {
362                String queryExtensionUrl = SubscriptionConstants.SUBSCRIPTION_TOPIC_R4_EXT_QUERY_CRITERIA;
363
364                // Try to find existing extension
365                return myCurrentResourceTrigger.getExtension().stream()
366                                .filter(extension -> queryExtensionUrl.equals(extension.getUrl()))
367                                .findFirst()
368                                .orElseGet(() -> {
369                                        // Create and add new extension if none exists
370                                        Extension queryCriteria = new Extension().setUrl(queryExtensionUrl);
371                                        myCurrentResourceTrigger.addExtension(queryCriteria);
372                                        return queryCriteria;
373                                });
374        }
375
376        private void addExtension(String theUrl, org.hl7.fhir.r4.model.Type theValue) {
377                Extension extension = new Extension().setUrl(theUrl).setValue(theValue);
378                myTopic.addExtension(extension);
379        }
380
381        private void addNestedExtension(Extension theParent, String theUrl, org.hl7.fhir.r4.model.Type theValue) {
382                Extension nestedExtension = new Extension().setUrl(theUrl).setValue(theValue);
383                theParent.addExtension(nestedExtension);
384        }
385}