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