001/*-
002 * #%L
003 * hapi-fhir-server-openapi
004 * %%
005 * Copyright (C) 2014 - 2024 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.rest.openapi;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.FhirVersionEnum;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.api.Hook;
027import ca.uhn.fhir.interceptor.api.Pointcut;
028import ca.uhn.fhir.rest.api.Constants;
029import ca.uhn.fhir.rest.server.IServerAddressStrategy;
030import ca.uhn.fhir.rest.server.IServerConformanceProvider;
031import ca.uhn.fhir.rest.server.RestfulServer;
032import ca.uhn.fhir.rest.server.RestfulServerUtils;
033import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
034import ca.uhn.fhir.util.ClasspathUtil;
035import ca.uhn.fhir.util.ExtensionConstants;
036import ca.uhn.fhir.util.HapiExtensions;
037import ca.uhn.fhir.util.UrlUtil;
038import com.google.common.collect.ImmutableList;
039import com.vladsch.flexmark.html.HtmlRenderer;
040import com.vladsch.flexmark.parser.Parser;
041import io.swagger.v3.core.util.Yaml;
042import io.swagger.v3.oas.models.Components;
043import io.swagger.v3.oas.models.OpenAPI;
044import io.swagger.v3.oas.models.Operation;
045import io.swagger.v3.oas.models.PathItem;
046import io.swagger.v3.oas.models.Paths;
047import io.swagger.v3.oas.models.examples.Example;
048import io.swagger.v3.oas.models.info.Contact;
049import io.swagger.v3.oas.models.info.Info;
050import io.swagger.v3.oas.models.media.Content;
051import io.swagger.v3.oas.models.media.DateSchema;
052import io.swagger.v3.oas.models.media.DateTimeSchema;
053import io.swagger.v3.oas.models.media.MediaType;
054import io.swagger.v3.oas.models.media.NumberSchema;
055import io.swagger.v3.oas.models.media.ObjectSchema;
056import io.swagger.v3.oas.models.media.Schema;
057import io.swagger.v3.oas.models.media.StringSchema;
058import io.swagger.v3.oas.models.parameters.Parameter;
059import io.swagger.v3.oas.models.parameters.RequestBody;
060import io.swagger.v3.oas.models.responses.ApiResponse;
061import io.swagger.v3.oas.models.responses.ApiResponses;
062import io.swagger.v3.oas.models.servers.Server;
063import io.swagger.v3.oas.models.tags.Tag;
064import jakarta.annotation.Nonnull;
065import jakarta.servlet.ServletContext;
066import jakarta.servlet.http.HttpServletRequest;
067import jakarta.servlet.http.HttpServletResponse;
068import org.apache.commons.io.IOUtils;
069import org.apache.commons.lang3.StringUtils;
070import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_40;
071import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
072import org.hl7.fhir.convertors.factory.VersionConvertorFactory_43_50;
073import org.hl7.fhir.instance.model.api.IBaseConformance;
074import org.hl7.fhir.instance.model.api.IBaseResource;
075import org.hl7.fhir.instance.model.api.IPrimitiveType;
076import org.hl7.fhir.r4.model.CanonicalType;
077import org.hl7.fhir.r4.model.CapabilityStatement;
078import org.hl7.fhir.r4.model.CodeableConcept;
079import org.hl7.fhir.r4.model.Coding;
080import org.hl7.fhir.r4.model.DateType;
081import org.hl7.fhir.r4.model.Enumerations;
082import org.hl7.fhir.r4.model.Extension;
083import org.hl7.fhir.r4.model.IdType;
084import org.hl7.fhir.r4.model.OperationDefinition;
085import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent;
086import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse;
087import org.hl7.fhir.r4.model.Parameters;
088import org.hl7.fhir.r4.model.Reference;
089import org.hl7.fhir.r4.model.Resource;
090import org.hl7.fhir.r4.model.StringType;
091import org.hl7.fhir.r4.model.Type;
092import org.hl7.fhir.r4.model.codesystems.DataTypes;
093import org.thymeleaf.IEngineConfiguration;
094import org.thymeleaf.TemplateEngine;
095import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
096import org.thymeleaf.cache.ICacheEntryValidity;
097import org.thymeleaf.context.IExpressionContext;
098import org.thymeleaf.context.WebContext;
099import org.thymeleaf.linkbuilder.AbstractLinkBuilder;
100import org.thymeleaf.standard.StandardDialect;
101import org.thymeleaf.templatemode.TemplateMode;
102import org.thymeleaf.templateresolver.ITemplateResolver;
103import org.thymeleaf.templateresolver.TemplateResolution;
104import org.thymeleaf.templateresource.ClassLoaderTemplateResource;
105import org.thymeleaf.web.servlet.IServletWebExchange;
106import org.thymeleaf.web.servlet.JakartaServletWebApplication;
107
108import java.io.IOException;
109import java.io.InputStream;
110import java.math.BigDecimal;
111import java.nio.charset.StandardCharsets;
112import java.util.ArrayList;
113import java.util.HashMap;
114import java.util.Iterator;
115import java.util.LinkedHashMap;
116import java.util.List;
117import java.util.Map;
118import java.util.Optional;
119import java.util.Properties;
120import java.util.Set;
121import java.util.function.Supplier;
122import java.util.stream.Collectors;
123
124import static ca.uhn.fhir.rest.server.util.NarrativeUtil.sanitizeHtmlFragment;
125import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
126import static org.apache.commons.lang3.StringUtils.defaultString;
127import static org.apache.commons.lang3.StringUtils.isBlank;
128import static org.apache.commons.lang3.StringUtils.isNotBlank;
129
130public class OpenApiInterceptor {
131
132        public static final String FHIR_JSON_RESOURCE = "FHIR-JSON-RESOURCE";
133        public static final String FHIR_XML_RESOURCE = "FHIR-XML-RESOURCE";
134        public static final String PAGE_SYSTEM = "System Level Operations";
135        public static final String PAGE_ALL = "All";
136        public static final FhirContext FHIR_CONTEXT_CANONICAL = FhirContext.forR4();
137        public static final String REQUEST_DETAILS = "REQUEST_DETAILS";
138        public static final String RACCOON_PNG = "raccoon.png";
139        private final String mySwaggerUiVersion;
140        private final TemplateEngine myTemplateEngine;
141        private final Parser myFlexmarkParser;
142        private final HtmlRenderer myFlexmarkRenderer;
143        private final Map<String, String> myResourcePathToClasspath = new HashMap<>();
144        private final Map<String, String> myExtensionToContentType = new HashMap<>();
145        private String myBannerImage;
146        private String myCssText;
147        private boolean myUseResourcePages;
148
149        /**
150         * Constructor
151         */
152        public OpenApiInterceptor() {
153                mySwaggerUiVersion = initSwaggerUiWebJar();
154
155                myTemplateEngine = new TemplateEngine();
156                ITemplateResolver resolver = new SwaggerUiTemplateResolver();
157                myTemplateEngine.setTemplateResolver(resolver);
158                StandardDialect dialect = new StandardDialect();
159                myTemplateEngine.setDialect(dialect);
160
161                myTemplateEngine.setLinkBuilder(new TemplateLinkBuilder());
162
163                myFlexmarkParser = Parser.builder().build();
164                myFlexmarkRenderer = HtmlRenderer.builder().build();
165
166                initResources();
167        }
168
169        private void initResources() {
170                setBannerImage(RACCOON_PNG);
171                setUseResourcePages(true);
172
173                addResourcePathToClasspath("/swagger-ui/index.html", "/ca/uhn/fhir/rest/openapi/index.html");
174                addResourcePathToClasspath("/swagger-ui/" + RACCOON_PNG, "/ca/uhn/fhir/rest/openapi/raccoon.png");
175                addResourcePathToClasspath("/swagger-ui/index.css", "/ca/uhn/fhir/rest/openapi/index.css");
176
177                myExtensionToContentType.put(".png", "image/png");
178                myExtensionToContentType.put(".css", "text/css; charset=UTF-8");
179        }
180
181        protected void addResourcePathToClasspath(String thePath, String theClasspath) {
182                myResourcePathToClasspath.put(thePath, theClasspath);
183        }
184
185        private String initSwaggerUiWebJar() {
186                final String mySwaggerUiVersion;
187                Properties props = new Properties();
188                String resourceName = "/META-INF/maven/org.webjars/swagger-ui/pom.properties";
189                try {
190                        InputStream resourceAsStream = ClasspathUtil.loadResourceAsStream(resourceName);
191                        props.load(resourceAsStream);
192                } catch (IOException e) {
193                        throw new ConfigurationException(Msg.code(239) + "Failed to load resource: " + resourceName);
194                }
195                mySwaggerUiVersion = props.getProperty("version");
196                return mySwaggerUiVersion;
197        }
198
199        @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED)
200        public boolean serveSwaggerUi(
201                        HttpServletRequest theRequest, HttpServletResponse theResponse, ServletRequestDetails theRequestDetails)
202                        throws IOException {
203                String requestPath = theRequest.getPathInfo();
204                String queryString = theRequest.getQueryString();
205
206                if (isBlank(requestPath) || requestPath.equals("/")) {
207                        if (isBlank(queryString)) {
208                                Set<String> highestRankedAcceptValues =
209                                                RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theRequest);
210                                if (highestRankedAcceptValues.contains(Constants.CT_HTML)) {
211
212                                        String serverBase = ".";
213                                        if (theRequestDetails.getServletRequest() != null) {
214                                                IServerAddressStrategy addressStrategy =
215                                                                theRequestDetails.getServer().getServerAddressStrategy();
216                                                serverBase = addressStrategy.determineServerBase(theRequest.getServletContext(), theRequest);
217                                        }
218                                        String redirectUrl = theResponse.encodeRedirectURL(serverBase + "/swagger-ui/");
219                                        theResponse.sendRedirect(redirectUrl);
220                                        theResponse.getWriter().close();
221                                        return false;
222                                }
223                        }
224
225                        return true;
226                }
227
228                if (requestPath.startsWith("/swagger-ui/")) {
229
230                        return !handleResourceRequest(theResponse, theRequestDetails, requestPath);
231
232                } else if (requestPath.equals("/api-docs")) {
233
234                        OpenAPI openApi = generateOpenApi(theRequestDetails);
235                        String response = Yaml.pretty(openApi);
236
237                        theResponse.setContentType("text/yaml");
238                        theResponse.setStatus(200);
239                        theResponse.getWriter().write(response);
240                        theResponse.getWriter().close();
241                        return false;
242                }
243
244                return true;
245        }
246
247        protected boolean handleResourceRequest(
248                        HttpServletResponse theResponse, ServletRequestDetails theRequestDetails, String requestPath)
249                        throws IOException {
250                if (requestPath.equals("/swagger-ui/") || requestPath.equals("/swagger-ui/index.html")) {
251                        serveSwaggerUiHtml(theRequestDetails, theResponse);
252                        return true;
253                }
254
255                String resourceClasspath = myResourcePathToClasspath.get(requestPath);
256                if (resourceClasspath != null) {
257                        theResponse.setStatus(200);
258
259                        String extension = requestPath.substring(requestPath.lastIndexOf('.'));
260                        String contentType = myExtensionToContentType.get(extension);
261                        assert contentType != null;
262                        theResponse.setContentType(contentType);
263                        try (InputStream resource = ClasspathUtil.loadResourceAsStream(resourceClasspath)) {
264                                IOUtils.copy(resource, theResponse.getOutputStream());
265                                theResponse.getOutputStream().close();
266                        }
267                        return true;
268                }
269
270                String resourcePath = requestPath.substring("/swagger-ui/".length());
271
272                if (resourcePath.equals("swagger-ui-custom.css") && isNotBlank(myCssText)) {
273                        theResponse.setContentType("text/css");
274                        theResponse.setStatus(200);
275                        theResponse.getWriter().println(myCssText);
276                        theResponse.getWriter().close();
277                        return true;
278                }
279
280                try (InputStream resource = ClasspathUtil.loadResourceAsStream(
281                                "/META-INF/resources/webjars/swagger-ui/" + mySwaggerUiVersion + "/" + resourcePath)) {
282
283                        if (resourcePath.endsWith(".js") || resourcePath.endsWith(".map")) {
284                                theResponse.setContentType("application/javascript");
285                                theResponse.setStatus(200);
286                                IOUtils.copy(resource, theResponse.getOutputStream());
287                                theResponse.getOutputStream().close();
288                                return true;
289                        }
290
291                        if (resourcePath.endsWith(".css")) {
292                                theResponse.setContentType("text/css");
293                                theResponse.setStatus(200);
294                                IOUtils.copy(resource, theResponse.getOutputStream());
295                                theResponse.getOutputStream().close();
296                                return true;
297                        }
298
299                        if (resourcePath.endsWith(".html")) {
300                                theResponse.setContentType(Constants.CT_HTML);
301                                theResponse.setStatus(200);
302                                IOUtils.copy(resource, theResponse.getOutputStream());
303                                theResponse.getOutputStream().close();
304                                return true;
305                        }
306                }
307                return false;
308        }
309
310        public String removeTrailingSlash(String theUrl) {
311                while (theUrl != null && theUrl.endsWith("/")) {
312                        theUrl = theUrl.substring(0, theUrl.length() - 1);
313                }
314                return theUrl;
315        }
316
317        /**
318         * If supplied, this field can be used to provide additional CSS text that should
319         * be loaded by the swagger-ui page. The contents should be raw CSS text, e.g.
320         * <code>
321         * BODY { font-size: 1.1em; }
322         * </code>
323         */
324        public String getCssText() {
325                return myCssText;
326        }
327
328        /**
329         * If supplied, this field can be used to provide additional CSS text that should
330         * be loaded by the swagger-ui page. The contents should be raw CSS text, e.g.
331         * <code>
332         * BODY { font-size: 1.1em; }
333         * </code>
334         */
335        public void setCssText(String theCssText) {
336                myCssText = theCssText;
337        }
338
339        @SuppressWarnings("unchecked")
340        private void serveSwaggerUiHtml(ServletRequestDetails theRequestDetails, HttpServletResponse theResponse)
341                        throws IOException {
342                CapabilityStatement cs = getCapabilityStatement(theRequestDetails);
343                String baseUrl = removeTrailingSlash(cs.getImplementation().getUrl());
344                theResponse.setStatus(200);
345                theResponse.setContentType(Constants.CT_HTML);
346
347                HttpServletRequest servletRequest = theRequestDetails.getServletRequest();
348                ServletContext servletContext = servletRequest.getServletContext();
349
350                JakartaServletWebApplication application = JakartaServletWebApplication.buildApplication(servletContext);
351                IServletWebExchange exchange = application.buildExchange(servletRequest, theResponse);
352                WebContext context = new WebContext(exchange);
353                context.setVariable(REQUEST_DETAILS, theRequestDetails);
354                context.setVariable("DESCRIPTION", cs.getImplementation().getDescription());
355                context.setVariable("SERVER_NAME", cs.getSoftware().getName());
356                context.setVariable("SERVER_VERSION", cs.getSoftware().getVersion());
357                context.setVariable("BASE_URL", cs.getImplementation().getUrl());
358                context.setVariable("BANNER_IMAGE_URL", getBannerImage());
359                context.setVariable("OPENAPI_DOCS", baseUrl + "/api-docs");
360                context.setVariable("FHIR_VERSION", cs.getFhirVersion().toCode());
361                context.setVariable("ADDITIONAL_CSS_TEXT", getCssText());
362                context.setVariable("USE_RESOURCE_PAGES", isUseResourcePages());
363                context.setVariable(
364                                "FHIR_VERSION_CODENAME",
365                                FhirVersionEnum.forVersionString(cs.getFhirVersion().toCode()).name());
366
367                String copyright = cs.getCopyright();
368                if (isNotBlank(copyright)) {
369                        copyright = renderMarkdown(copyright);
370                        context.setVariable("COPYRIGHT_HTML", copyright);
371                }
372
373                List<String> pageNames = new ArrayList<>();
374                Map<String, Integer> resourceToCount = new HashMap<>();
375                cs.getRestFirstRep().getResource().stream().forEach(t -> {
376                        String type = t.getType();
377                        pageNames.add(type);
378                        Extension countExtension = t.getExtensionByUrl(ExtensionConstants.CONF_RESOURCE_COUNT);
379                        if (countExtension != null) {
380                                IPrimitiveType<? extends Number> countExtensionValue =
381                                                (IPrimitiveType<? extends Number>) countExtension.getValueAsPrimitive();
382                                if (countExtensionValue != null && countExtensionValue.hasValue()) {
383                                        resourceToCount.put(type, countExtensionValue.getValue().intValue());
384                                }
385                        }
386                });
387                pageNames.sort((o1, o2) -> {
388                        Integer count1 = resourceToCount.get(o1);
389                        Integer count2 = resourceToCount.get(o2);
390                        if (count1 != null && count2 != null) {
391                                return count2 - count1;
392                        }
393                        if (count1 != null) {
394                                return -1;
395                        }
396                        if (count2 != null) {
397                                return 1;
398                        }
399                        return o1.compareTo(o2);
400                });
401
402                pageNames.add(0, PAGE_ALL);
403                pageNames.add(1, PAGE_SYSTEM);
404
405                context.setVariable("PAGE_NAMES", pageNames);
406                context.setVariable("PAGE_NAME_TO_COUNT", resourceToCount);
407
408                String page;
409                if (isUseResourcePages()) {
410                        page = extractPageName(theRequestDetails, PAGE_SYSTEM);
411                } else {
412                        page = PAGE_ALL;
413                }
414                context.setVariable("PAGE", page);
415
416                populateOIDCVariables(theRequestDetails, context);
417
418                String outcome = myTemplateEngine.process("index.html", context);
419
420                theResponse.getWriter().write(outcome);
421                theResponse.getWriter().close();
422        }
423
424        @Nonnull
425        private String renderMarkdown(String copyright) {
426                return myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright));
427        }
428
429        protected void populateOIDCVariables(ServletRequestDetails theRequestDetails, WebContext theContext) {
430                theContext.setVariable("OAUTH2_REDIRECT_URL_PROPERTY", "");
431        }
432
433        private String extractPageName(ServletRequestDetails theRequestDetails, String theDefault) {
434                String[] pageValues = theRequestDetails.getParameters().get("page");
435                String page = null;
436                if (pageValues != null && pageValues.length > 0) {
437                        page = pageValues[0];
438                }
439                if (isBlank(page)) {
440                        page = theDefault;
441                }
442                return page;
443        }
444
445        protected OpenAPI generateOpenApi(ServletRequestDetails theRequestDetails) {
446                String page = extractPageName(theRequestDetails, null);
447
448                CapabilityStatement cs = getCapabilityStatement(theRequestDetails);
449                FhirContext ctx = theRequestDetails.getFhirContext();
450
451                IServerConformanceProvider<?> capabilitiesProvider = null;
452                RestfulServer restfulServer = theRequestDetails.getServer();
453                if (restfulServer.getServerConformanceProvider() instanceof IServerConformanceProvider) {
454                        capabilitiesProvider = (IServerConformanceProvider<?>) restfulServer.getServerConformanceProvider();
455                }
456
457                OpenAPI openApi = new OpenAPI();
458
459                openApi.setInfo(new Info());
460                openApi.getInfo().setDescription(cs.getDescription());
461                openApi.getInfo().setTitle(cs.getSoftware().getName());
462                openApi.getInfo().setVersion(cs.getSoftware().getVersion());
463                openApi.getInfo().setContact(new Contact());
464                openApi.getInfo().getContact().setName(cs.getContactFirstRep().getName());
465                openApi.getInfo()
466                                .getContact()
467                                .setEmail(cs.getContactFirstRep().getTelecomFirstRep().getValue());
468
469                Server server = new Server();
470                openApi.addServersItem(server);
471                server.setUrl(cs.getImplementation().getUrl());
472                server.setDescription(cs.getSoftware().getName());
473
474                Paths paths = new Paths();
475                openApi.setPaths(paths);
476
477                if (page == null || page.equals(PAGE_SYSTEM) || page.equals(PAGE_ALL)) {
478                        Tag serverTag = new Tag();
479                        serverTag.setName(PAGE_SYSTEM);
480                        serverTag.setDescription("Server-level operations");
481                        openApi.addTagsItem(serverTag);
482
483                        Operation capabilitiesOperation = getPathItem(paths, "/metadata", PathItem.HttpMethod.GET);
484                        capabilitiesOperation.addTagsItem(PAGE_SYSTEM);
485                        capabilitiesOperation.setSummary("server-capabilities: Fetch the server FHIR CapabilityStatement");
486                        addFhirResourceResponse(ctx, openApi, capabilitiesOperation, "CapabilityStatement");
487
488                        Set<CapabilityStatement.SystemRestfulInteraction> systemInteractions =
489                                        cs.getRestFirstRep().getInteraction().stream()
490                                                        .map(t -> t.getCode())
491                                                        .collect(Collectors.toSet());
492                        // Transaction Operation
493                        if (systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.TRANSACTION)
494                                        || systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.BATCH)) {
495                                Operation transaction = getPathItem(paths, "/", PathItem.HttpMethod.POST);
496                                transaction.addTagsItem(PAGE_SYSTEM);
497                                transaction.setSummary("server-transaction: Execute a FHIR Transaction (or FHIR Batch) Bundle");
498                                addFhirResourceResponse(ctx, openApi, transaction, null);
499                                addFhirResourceRequestBody(openApi, transaction, ctx, null);
500                        }
501
502                        // System History Operation
503                        if (systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.HISTORYSYSTEM)) {
504                                Operation systemHistory = getPathItem(paths, "/_history", PathItem.HttpMethod.GET);
505                                systemHistory.addTagsItem(PAGE_SYSTEM);
506                                systemHistory.setSummary(
507                                                "server-history: Fetch the resource change history across all resource types on the server");
508                                addFhirResourceResponse(ctx, openApi, systemHistory, null);
509                        }
510
511                        // System-level Operations
512                        for (CapabilityStatement.CapabilityStatementRestResourceOperationComponent nextOperation :
513                                        cs.getRestFirstRep().getOperation()) {
514                                addFhirOperation(ctx, openApi, theRequestDetails, capabilitiesProvider, paths, null, nextOperation);
515                        }
516                }
517
518                for (CapabilityStatement.CapabilityStatementRestResourceComponent nextResource :
519                                cs.getRestFirstRep().getResource()) {
520                        String resourceType = nextResource.getType();
521
522                        if (page != null && !page.equals(resourceType) && !page.equals(PAGE_ALL)) {
523                                continue;
524                        }
525
526                        Set<CapabilityStatement.TypeRestfulInteraction> typeRestfulInteractions =
527                                        nextResource.getInteraction().stream()
528                                                        .map(t -> t.getCodeElement().getValue())
529                                                        .collect(Collectors.toSet());
530
531                        Tag resourceTag = new Tag();
532                        resourceTag.setName(resourceType);
533                        resourceTag.setDescription(createResourceDescription(nextResource));
534                        openApi.addTagsItem(resourceTag);
535
536                        // Instance Read
537                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.READ)) {
538                                Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.GET);
539                                operation.addTagsItem(resourceType);
540                                operation.setSummary("read-instance: Read " + resourceType + " instance");
541                                addResourceIdParameter(operation);
542                                addFhirResourceResponse(ctx, openApi, operation, null);
543                        }
544
545                        // Instance VRead
546                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.VREAD)) {
547                                Operation operation =
548                                                getPathItem(paths, "/" + resourceType + "/{id}/_history/{version_id}", PathItem.HttpMethod.GET);
549                                operation.addTagsItem(resourceType);
550                                operation.setSummary("vread-instance: Read " + resourceType + " instance with specific version");
551                                addResourceIdParameter(operation);
552                                addResourceVersionIdParameter(operation);
553                                addFhirResourceResponse(ctx, openApi, operation, null);
554                        }
555
556                        // Type Create
557                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.CREATE)) {
558                                Operation operation = getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.POST);
559                                operation.addTagsItem(resourceType);
560                                operation.setSummary("create-type: Create a new " + resourceType + " instance");
561                                addFhirResourceRequestBody(openApi, operation, ctx, genericExampleSupplier(ctx, resourceType));
562                                addFhirResourceResponse(ctx, openApi, operation, null);
563                        }
564
565                        // Instance Update
566                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.UPDATE)) {
567                                Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.PUT);
568                                operation.addTagsItem(resourceType);
569                                operation.setSummary("update-instance: Update an existing " + resourceType
570                                                + " instance, or create using a client-assigned ID");
571                                addResourceIdParameter(operation);
572                                addFhirResourceRequestBody(openApi, operation, ctx, genericExampleSupplier(ctx, resourceType));
573                                addFhirResourceResponse(ctx, openApi, operation, null);
574                        }
575
576                        // Type history
577                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.HISTORYTYPE)) {
578                                Operation operation = getPathItem(paths, "/" + resourceType + "/_history", PathItem.HttpMethod.GET);
579                                operation.addTagsItem(resourceType);
580                                operation.setSummary(
581                                                "type-history: Fetch the resource change history for all resources of type " + resourceType);
582                                addFhirResourceResponse(ctx, openApi, operation, null);
583                        }
584
585                        // Instance history
586                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.HISTORYTYPE)) {
587                                Operation operation =
588                                                getPathItem(paths, "/" + resourceType + "/{id}/_history", PathItem.HttpMethod.GET);
589                                operation.addTagsItem(resourceType);
590                                operation.setSummary("instance-history: Fetch the resource change history for all resources of type "
591                                                + resourceType);
592                                addResourceIdParameter(operation);
593                                addFhirResourceResponse(ctx, openApi, operation, null);
594                        }
595
596                        // Instance Patch
597                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.PATCH)) {
598                                Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.PATCH);
599                                operation.addTagsItem(resourceType);
600                                operation.setSummary("instance-patch: Patch a resource instance of type " + resourceType + " by ID");
601                                addResourceIdParameter(operation);
602                                addFhirResourceRequestBody(openApi, operation, FHIR_CONTEXT_CANONICAL, patchExampleSupplier());
603                                addFhirResourceResponse(ctx, openApi, operation, null);
604                        }
605
606                        // Instance Delete
607                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.DELETE)) {
608                                Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.DELETE);
609                                operation.addTagsItem(resourceType);
610                                operation.setSummary("instance-delete: Perform a logical delete on a resource instance");
611                                addResourceIdParameter(operation);
612                                addFhirResourceResponse(ctx, openApi, operation, null);
613                        }
614
615                        // Search
616                        if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.SEARCHTYPE)) {
617                                addSearchOperation(
618                                                openApi,
619                                                getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.GET),
620                                                ctx,
621                                                resourceType,
622                                                nextResource);
623                                addSearchOperation(
624                                                openApi,
625                                                getPathItem(paths, "/" + resourceType + "/_search", PathItem.HttpMethod.GET),
626                                                ctx,
627                                                resourceType,
628                                                nextResource);
629                        }
630
631                        // Resource-level Operations
632                        for (CapabilityStatement.CapabilityStatementRestResourceOperationComponent nextOperation :
633                                        nextResource.getOperation()) {
634                                addFhirOperation(
635                                                ctx, openApi, theRequestDetails, capabilitiesProvider, paths, resourceType, nextOperation);
636                        }
637                }
638
639                return openApi;
640        }
641
642        @Nonnull
643        protected String createResourceDescription(
644                        CapabilityStatement.CapabilityStatementRestResourceComponent theResource) {
645                StringBuilder b = new StringBuilder();
646                b.append("The ").append(theResource.getType()).append(" FHIR resource type");
647
648                String documentation = theResource.getDocumentation();
649                if (isNotBlank(documentation)) {
650                        b.append("<br/>");
651                        b.append(sanitizeHtmlFragment(renderMarkdown(documentation)));
652                }
653
654                if (isNotBlank(theResource.getProfile())) {
655                        b.append("<br/>");
656                        b.append("Base profile: ");
657                        b.append(sanitizeHtmlFragment(theResource.getProfile()));
658                }
659
660                for (CanonicalType next : theResource.getSupportedProfile()) {
661                        String nextSupportedProfile = next.getValueAsString();
662                        if (isNotBlank(nextSupportedProfile)) {
663                                b.append("<br/>");
664                                b.append("Supported profile: ");
665                                b.append(sanitizeHtmlFragment(nextSupportedProfile));
666                        }
667                }
668
669                return b.toString();
670        }
671
672        protected void addSearchOperation(
673                        final OpenAPI openApi,
674                        final Operation operation,
675                        final FhirContext ctx,
676                        final String resourceType,
677                        final CapabilityStatement.CapabilityStatementRestResourceComponent nextResource) {
678                operation.addTagsItem(resourceType);
679                operation.setDescription("This is a search type");
680                operation.setSummary("search-type: Search for " + resourceType + " instances");
681                addFhirResourceResponse(ctx, openApi, operation, null);
682
683                for (final CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent nextSearchParam :
684                                nextResource.getSearchParam()) {
685                        final Parameter parametersItem = new Parameter();
686                        operation.addParametersItem(parametersItem);
687
688                        parametersItem.setName(nextSearchParam.getName());
689                        parametersItem.setRequired(false);
690                        parametersItem.setIn("query");
691                        parametersItem.setDescription(nextSearchParam.getDocumentation());
692                        parametersItem.setSchema(toSchema(nextSearchParam.getType()));
693                }
694        }
695
696        private Supplier<IBaseResource> patchExampleSupplier() {
697                return () -> {
698                        Parameters example = new Parameters();
699                        Parameters.ParametersParameterComponent operation =
700                                        example.addParameter().setName("operation");
701                        operation.addPart().setName("type").setValue(new StringType("add"));
702                        operation.addPart().setName("path").setValue(new StringType("Patient"));
703                        operation.addPart().setName("name").setValue(new StringType("birthDate"));
704                        operation.addPart().setName("value").setValue(new DateType("1930-01-01"));
705                        return example;
706                };
707        }
708
709        private void addSchemaFhirResource(OpenAPI theOpenApi) {
710                ensureComponentsSchemasPopulated(theOpenApi);
711
712                if (!theOpenApi.getComponents().getSchemas().containsKey(FHIR_JSON_RESOURCE)) {
713                        ObjectSchema fhirJsonSchema = new ObjectSchema();
714                        fhirJsonSchema.setDescription("A FHIR resource");
715                        theOpenApi.getComponents().addSchemas(FHIR_JSON_RESOURCE, fhirJsonSchema);
716                }
717
718                if (!theOpenApi.getComponents().getSchemas().containsKey(FHIR_XML_RESOURCE)) {
719                        ObjectSchema fhirXmlSchema = new ObjectSchema();
720                        fhirXmlSchema.setDescription("A FHIR resource");
721                        theOpenApi.getComponents().addSchemas(FHIR_XML_RESOURCE, fhirXmlSchema);
722                }
723        }
724
725        private void ensureComponentsSchemasPopulated(OpenAPI theOpenApi) {
726                if (theOpenApi.getComponents() == null) {
727                        theOpenApi.setComponents(new Components());
728                }
729                if (theOpenApi.getComponents().getSchemas() == null) {
730                        theOpenApi.getComponents().setSchemas(new LinkedHashMap<>());
731                }
732        }
733
734        private CapabilityStatement getCapabilityStatement(ServletRequestDetails theRequestDetails) {
735                RestfulServer restfulServer = theRequestDetails.getServer();
736                IBaseConformance versionIndependentCapabilityStatement =
737                                restfulServer.getCapabilityStatement(theRequestDetails);
738                return toCanonicalVersion(versionIndependentCapabilityStatement);
739        }
740
741        private void addFhirOperation(
742                        FhirContext theFhirContext,
743                        OpenAPI theOpenApi,
744                        ServletRequestDetails theRequestDetails,
745                        IServerConformanceProvider<?> theCapabilitiesProvider,
746                        Paths thePaths,
747                        String theResourceType,
748                        CapabilityStatement.CapabilityStatementRestResourceOperationComponent theOperation) {
749                if (theCapabilitiesProvider != null) {
750                        IdType definitionId = new IdType(theOperation.getDefinition());
751                        IBaseResource operationDefinitionNonCanonical =
752                                        theCapabilitiesProvider.readOperationDefinition(definitionId, theRequestDetails);
753                        if (operationDefinitionNonCanonical == null) {
754                                return;
755                        }
756
757                        OperationDefinition operationDefinition = toCanonicalVersion(operationDefinitionNonCanonical);
758                        final boolean postOnly = operationDefinition.getAffectsState()
759                                        || operationDefinition.getParameter().stream()
760                                                        .filter(p -> p.getUse().equals(OperationParameterUse.IN))
761                                                        .anyMatch(p -> {
762                                                                final boolean required = p.getMin() > 0;
763                                                                return required && !isPrimitive(p);
764                                                        });
765
766                        if (!postOnly) {
767
768                                // GET form for non-state-affecting operations
769                                if (theResourceType != null) {
770                                        if (operationDefinition.getType()) {
771                                                Operation operation = getPathItem(
772                                                                thePaths,
773                                                                "/" + theResourceType + "/$" + operationDefinition.getCode(),
774                                                                PathItem.HttpMethod.GET);
775                                                populateOperation(
776                                                                theFhirContext,
777                                                                theOpenApi,
778                                                                theResourceType,
779                                                                operationDefinition,
780                                                                operation,
781                                                                "/" + theResourceType + "/$" + operationDefinition.getCode(),
782                                                                PathItem.HttpMethod.GET);
783                                        }
784                                        if (operationDefinition.getInstance()) {
785                                                Operation operation = getPathItem(
786                                                                thePaths,
787                                                                "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(),
788                                                                PathItem.HttpMethod.GET);
789                                                addResourceIdParameter(operation);
790                                                populateOperation(
791                                                                theFhirContext,
792                                                                theOpenApi,
793                                                                theResourceType,
794                                                                operationDefinition,
795                                                                operation,
796                                                                "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(),
797                                                                PathItem.HttpMethod.GET);
798                                        }
799                                } else {
800                                        if (operationDefinition.getSystem()) {
801                                                Operation operation =
802                                                                getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET);
803                                                populateOperation(
804                                                                theFhirContext,
805                                                                theOpenApi,
806                                                                null,
807                                                                operationDefinition,
808                                                                operation,
809                                                                "/$" + operationDefinition.getCode(),
810                                                                PathItem.HttpMethod.GET);
811                                        }
812                                }
813                        }
814
815                        // POST form for all operations
816                        if (theResourceType != null) {
817                                if (operationDefinition.getType()) {
818                                        Operation operation = getPathItem(
819                                                        thePaths,
820                                                        "/" + theResourceType + "/$" + operationDefinition.getCode(),
821                                                        PathItem.HttpMethod.POST);
822                                        populateOperation(
823                                                        theFhirContext,
824                                                        theOpenApi,
825                                                        theResourceType,
826                                                        operationDefinition,
827                                                        operation,
828                                                        "/" + theResourceType + "/$" + operationDefinition.getCode(),
829                                                        PathItem.HttpMethod.POST);
830                                }
831                                if (operationDefinition.getInstance()) {
832                                        Operation operation = getPathItem(
833                                                        thePaths,
834                                                        "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(),
835                                                        PathItem.HttpMethod.POST);
836                                        addResourceIdParameter(operation);
837                                        populateOperation(
838                                                        theFhirContext,
839                                                        theOpenApi,
840                                                        theResourceType,
841                                                        operationDefinition,
842                                                        operation,
843                                                        "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(),
844                                                        PathItem.HttpMethod.POST);
845                                }
846                        } else {
847                                if (operationDefinition.getSystem()) {
848                                        Operation operation =
849                                                        getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
850                                        populateOperation(
851                                                        theFhirContext,
852                                                        theOpenApi,
853                                                        null,
854                                                        operationDefinition,
855                                                        operation,
856                                                        "/$" + operationDefinition.getCode(),
857                                                        PathItem.HttpMethod.POST);
858                                }
859                        }
860                }
861        }
862
863        private static final List<String> primitiveTypes = List.of(
864                        DataTypes.BOOLEAN.toCode(),
865                        DataTypes.INTEGER.toCode(),
866                        DataTypes.STRING.toCode(),
867                        DataTypes.DECIMAL.toCode(),
868                        DataTypes.URI.toCode(),
869                        DataTypes.URL.toCode(),
870                        DataTypes.CANONICAL.toCode(),
871                        DataTypes.REFERENCE.toCode(),
872                        DataTypes.BASE64BINARY.toCode(),
873                        DataTypes.INSTANT.toCode(),
874                        DataTypes.DATE.toCode(),
875                        DataTypes.DATETIME.toCode(),
876                        DataTypes.TIME.toCode(),
877                        DataTypes.CODE.toCode(),
878                        DataTypes.CODING.toCode(),
879                        DataTypes.OID.toCode(),
880                        DataTypes.ID.toCode(),
881                        DataTypes.MARKDOWN.toCode(),
882                        DataTypes.UNSIGNEDINT.toCode(),
883                        DataTypes.POSITIVEINT.toCode(),
884                        DataTypes.UUID.toCode());
885
886        private static boolean isPrimitive(OperationDefinitionParameterComponent parameter) {
887                return parameter.getType() != null && primitiveTypes.contains(parameter.getType());
888        }
889
890        private void populateOperation(
891                        FhirContext theFhirContext,
892                        OpenAPI theOpenApi,
893                        String theResourceType,
894                        OperationDefinition theOperationDefinition,
895                        Operation theOperation,
896                        String thePath,
897                        PathItem.HttpMethod httpMethod) {
898                if (theResourceType == null) {
899                        theOperation.addTagsItem(PAGE_SYSTEM);
900                } else {
901                        theOperation.addTagsItem(theResourceType);
902                }
903                theOperation.setSummary(Optional.ofNullable(theOperationDefinition.getTitle())
904                                .orElse(String.format("%s: %s", httpMethod.name(), thePath)));
905                theOperation.setDescription(theOperationDefinition.getDescription());
906                addFhirResourceResponse(theFhirContext, theOpenApi, theOperation, null);
907                if (httpMethod == PathItem.HttpMethod.GET) {
908
909                        for (OperationDefinition.OperationDefinitionParameterComponent nextParameter :
910                                        theOperationDefinition.getParameter()) {
911                                if ("0".equals(nextParameter.getMax())
912                                                || !nextParameter.getUse().equals(OperationParameterUse.IN)) {
913                                        continue;
914                                }
915                                if (!isPrimitive(nextParameter) && nextParameter.getMin() == 0) {
916                                        continue;
917                                }
918                                Parameter parametersItem = new Parameter();
919                                theOperation.addParametersItem(parametersItem);
920
921                                parametersItem.setName(nextParameter.getName());
922                                parametersItem.setIn("query");
923                                parametersItem.setDescription(nextParameter.getDocumentation());
924                                parametersItem.setRequired(nextParameter.getMin() > 0);
925                                parametersItem.setSchema(toSchema(nextParameter.getSearchType()));
926
927                                List<Extension> exampleExtensions =
928                                                nextParameter.getExtensionsByUrl(HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE);
929                                if (exampleExtensions.size() == 1) {
930                                        parametersItem.setExample(
931                                                        exampleExtensions.get(0).getValueAsPrimitive().getValueAsString());
932                                } else if (exampleExtensions.size() > 1) {
933                                        for (Extension next : exampleExtensions) {
934                                                String nextExample = next.getValueAsPrimitive().getValueAsString();
935                                                parametersItem.addExample(nextExample, new Example().value(nextExample));
936                                        }
937                                }
938                        }
939
940                } else {
941
942                        Parameters exampleRequestBody = new Parameters();
943                        for (OperationDefinition.OperationDefinitionParameterComponent nextSearchParam :
944                                        theOperationDefinition.getParameter()) {
945                                if ("0".equals(nextSearchParam.getMax())
946                                                || !nextSearchParam.getUse().equals(OperationParameterUse.IN)) {
947                                        continue;
948                                }
949                                Parameters.ParametersParameterComponent param = exampleRequestBody.addParameter();
950                                param.setName(nextSearchParam.getName());
951                                String paramType = nextSearchParam.getType();
952                                switch (defaultString(paramType)) {
953                                        case "uri":
954                                        case "url":
955                                        case "code":
956                                        case "string": {
957                                                IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL
958                                                                .getElementDefinition(paramType)
959                                                                .newInstance();
960                                                type.setValueAsString("example");
961                                                param.setValue((Type) type);
962                                                break;
963                                        }
964                                        case "integer": {
965                                                IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL
966                                                                .getElementDefinition(paramType)
967                                                                .newInstance();
968                                                type.setValueAsString("0");
969                                                param.setValue((Type) type);
970                                                break;
971                                        }
972                                        case "boolean": {
973                                                IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL
974                                                                .getElementDefinition(paramType)
975                                                                .newInstance();
976                                                type.setValueAsString("false");
977                                                param.setValue((Type) type);
978                                                break;
979                                        }
980                                        case "CodeableConcept": {
981                                                CodeableConcept type = new CodeableConcept();
982                                                type.getCodingFirstRep().setSystem("http://example.com");
983                                                type.getCodingFirstRep().setCode("1234");
984                                                param.setValue(type);
985                                                break;
986                                        }
987                                        case "Coding": {
988                                                Coding type = new Coding();
989                                                type.setSystem("http://example.com");
990                                                type.setCode("1234");
991                                                param.setValue(type);
992                                                break;
993                                        }
994                                        case "Reference":
995                                                Reference reference = new Reference("example");
996                                                param.setValue(reference);
997                                                break;
998                                        case "Resource":
999                                                if (theResourceType != null) {
1000                                                        if (FHIR_CONTEXT_CANONICAL.getResourceTypes().contains(theResourceType)) {
1001                                                                IBaseResource resource = FHIR_CONTEXT_CANONICAL
1002                                                                                .getResourceDefinition(theResourceType)
1003                                                                                .newInstance();
1004                                                                resource.setId("1");
1005                                                                param.setResource((Resource) resource);
1006                                                        }
1007                                                }
1008                                                break;
1009                                }
1010                        }
1011
1012                        String exampleRequestBodyString = FHIR_CONTEXT_CANONICAL
1013                                        .newJsonParser()
1014                                        .setPrettyPrint(true)
1015                                        .encodeResourceToString(exampleRequestBody);
1016                        theOperation.setRequestBody(new RequestBody());
1017                        theOperation.getRequestBody().setContent(new Content());
1018                        MediaType mediaType = new MediaType();
1019                        mediaType.setExample(exampleRequestBodyString);
1020                        mediaType.setSchema(new Schema().type("object").title("FHIR Resource"));
1021                        theOperation.getRequestBody().getContent().addMediaType(Constants.CT_FHIR_JSON_NEW, mediaType);
1022                }
1023        }
1024
1025        protected Operation getPathItem(Paths thePaths, String thePath, PathItem.HttpMethod theMethod) {
1026                PathItem pathItem;
1027
1028                if (thePaths.containsKey(thePath)) {
1029                        pathItem = thePaths.get(thePath);
1030                } else {
1031                        pathItem = new PathItem();
1032                        thePaths.addPathItem(thePath, pathItem);
1033                }
1034
1035                switch (theMethod) {
1036                        case POST:
1037                                assert pathItem.getPost() == null : "Have duplicate POST at path: " + thePath;
1038                                return pathItem.post(new Operation()).getPost();
1039                        case GET:
1040                                assert pathItem.getGet() == null : "Have duplicate GET at path: " + thePath;
1041                                return pathItem.get(new Operation()).getGet();
1042                        case PUT:
1043                                assert pathItem.getPut() == null;
1044                                return pathItem.put(new Operation()).getPut();
1045                        case PATCH:
1046                                assert pathItem.getPatch() == null;
1047                                return pathItem.patch(new Operation()).getPatch();
1048                        case DELETE:
1049                                assert pathItem.getDelete() == null;
1050                                return pathItem.delete(new Operation()).getDelete();
1051                        case HEAD:
1052                        case OPTIONS:
1053                        case TRACE:
1054                        default:
1055                                throw new IllegalStateException(Msg.code(240));
1056                }
1057        }
1058
1059        private void addFhirResourceRequestBody(
1060                        OpenAPI theOpenApi,
1061                        Operation theOperation,
1062                        FhirContext theExampleFhirContext,
1063                        Supplier<IBaseResource> theExampleSupplier) {
1064                RequestBody requestBody = new RequestBody();
1065                requestBody.setContent(provideContentFhirResource(theOpenApi, theExampleFhirContext, theExampleSupplier));
1066                theOperation.setRequestBody(requestBody);
1067        }
1068
1069        private void addResourceVersionIdParameter(Operation theOperation) {
1070                Parameter parameter = new Parameter();
1071                parameter.setName("version_id");
1072                parameter.setIn("path");
1073                parameter.setDescription("The resource version ID");
1074                parameter.setExample("1");
1075                parameter.setRequired(true);
1076                parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1)));
1077                parameter.setStyle(Parameter.StyleEnum.SIMPLE);
1078                theOperation.addParametersItem(parameter);
1079        }
1080
1081        private void addFhirResourceResponse(
1082                        FhirContext theFhirContext, OpenAPI theOpenApi, Operation theOperation, String theResourceType) {
1083                theOperation.setResponses(new ApiResponses());
1084                ApiResponse response200 = new ApiResponse();
1085                response200.setDescription("Success");
1086                response200.setContent(provideContentFhirResource(
1087                                theOpenApi, theFhirContext, genericExampleSupplier(theFhirContext, theResourceType)));
1088                theOperation.getResponses().addApiResponse("200", response200);
1089        }
1090
1091        private Supplier<IBaseResource> genericExampleSupplier(FhirContext theFhirContext, String theResourceType) {
1092                if (theResourceType == null) {
1093                        return null;
1094                }
1095                return () -> {
1096                        IBaseResource example = null;
1097                        if (theResourceType != null && theFhirContext.getResourceTypes().contains(theResourceType)) {
1098                                example = theFhirContext.getResourceDefinition(theResourceType).newInstance();
1099                        }
1100                        return example;
1101                };
1102        }
1103
1104        private Content provideContentFhirResource(
1105                        OpenAPI theOpenApi, FhirContext theExampleFhirContext, Supplier<IBaseResource> theExampleSupplier) {
1106                addSchemaFhirResource(theOpenApi);
1107                Content retVal = new Content();
1108
1109                MediaType jsonSchema =
1110                                new MediaType().schema(new ObjectSchema().$ref("#/components/schemas/" + FHIR_JSON_RESOURCE));
1111                if (theExampleSupplier != null) {
1112                        jsonSchema.setExample(theExampleFhirContext
1113                                        .newJsonParser()
1114                                        .setPrettyPrint(true)
1115                                        .encodeResourceToString(theExampleSupplier.get()));
1116                }
1117                retVal.addMediaType(Constants.CT_FHIR_JSON_NEW, jsonSchema);
1118
1119                MediaType xmlSchema =
1120                                new MediaType().schema(new ObjectSchema().$ref("#/components/schemas/" + FHIR_XML_RESOURCE));
1121                if (theExampleSupplier != null) {
1122                        xmlSchema.setExample(theExampleFhirContext
1123                                        .newXmlParser()
1124                                        .setPrettyPrint(true)
1125                                        .encodeResourceToString(theExampleSupplier.get()));
1126                }
1127                retVal.addMediaType(Constants.CT_FHIR_XML_NEW, xmlSchema);
1128                return retVal;
1129        }
1130
1131        private void addResourceIdParameter(Operation theOperation) {
1132                Parameter parameter = new Parameter();
1133                parameter.setName("id");
1134                parameter.setIn("path");
1135                parameter.setDescription("The resource ID");
1136                parameter.setExample("123");
1137                parameter.setRequired(true);
1138                parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1)));
1139                parameter.setStyle(Parameter.StyleEnum.SIMPLE);
1140                theOperation.addParametersItem(parameter);
1141        }
1142
1143        protected ClassLoaderTemplateResource getIndexTemplate() {
1144                return new ClassLoaderTemplateResource(
1145                                myResourcePathToClasspath.get("/swagger-ui/index.html"), StandardCharsets.UTF_8.name());
1146        }
1147
1148        public String getBannerImage() {
1149                return myBannerImage;
1150        }
1151
1152        public OpenApiInterceptor setBannerImage(String theBannerImage) {
1153                myBannerImage = StringUtils.defaultIfBlank(theBannerImage, null);
1154                return this;
1155        }
1156
1157        public boolean isUseResourcePages() {
1158                return myUseResourcePages;
1159        }
1160
1161        public void setUseResourcePages(boolean theUseResourcePages) {
1162                myUseResourcePages = theUseResourcePages;
1163        }
1164
1165        @SuppressWarnings("unchecked")
1166        private static <T extends Resource> T toCanonicalVersion(IBaseResource theNonCanonical) {
1167                IBaseResource canonical;
1168                if (theNonCanonical instanceof org.hl7.fhir.dstu3.model.Resource) {
1169                        canonical =
1170                                        VersionConvertorFactory_30_40.convertResource((org.hl7.fhir.dstu3.model.Resource) theNonCanonical);
1171                } else if (theNonCanonical instanceof org.hl7.fhir.r5.model.Resource) {
1172                        canonical = VersionConvertorFactory_40_50.convertResource((org.hl7.fhir.r5.model.Resource) theNonCanonical);
1173                } else if (theNonCanonical instanceof org.hl7.fhir.r4b.model.Resource) {
1174                        org.hl7.fhir.r5.model.Resource r5 =
1175                                        VersionConvertorFactory_43_50.convertResource((org.hl7.fhir.r4b.model.Resource) theNonCanonical);
1176                        canonical = VersionConvertorFactory_40_50.convertResource(r5);
1177                } else {
1178                        canonical = theNonCanonical;
1179                }
1180                return (T) canonical;
1181        }
1182
1183        private class SwaggerUiTemplateResolver implements ITemplateResolver {
1184                @Override
1185                public String getName() {
1186                        return getClass().getName();
1187                }
1188
1189                @Override
1190                public Integer getOrder() {
1191                        return 0;
1192                }
1193
1194                @Override
1195                public TemplateResolution resolveTemplate(
1196                                IEngineConfiguration configuration,
1197                                String ownerTemplate,
1198                                String template,
1199                                Map<String, Object> templateResolutionAttributes) {
1200                        ClassLoaderTemplateResource resource = getIndexTemplate();
1201                        ICacheEntryValidity cacheValidity = new AlwaysValidCacheEntryValidity();
1202                        return new TemplateResolution(resource, TemplateMode.HTML, cacheValidity);
1203                }
1204        }
1205
1206        private static class TemplateLinkBuilder extends AbstractLinkBuilder {
1207
1208                @Override
1209                public String buildLink(
1210                                IExpressionContext theExpressionContext, String theBase, Map<String, Object> theParameters) {
1211
1212                        ServletRequestDetails requestDetails =
1213                                        (ServletRequestDetails) theExpressionContext.getVariable(REQUEST_DETAILS);
1214
1215                        IServerAddressStrategy addressStrategy = requestDetails.getServer().getServerAddressStrategy();
1216                        String baseUrl = addressStrategy.determineServerBase(
1217                                        requestDetails.getServletRequest().getServletContext(), requestDetails.getServletRequest());
1218
1219                        StringBuilder builder = new StringBuilder();
1220                        builder.append(baseUrl);
1221                        builder.append(theBase);
1222                        if (!theParameters.isEmpty()) {
1223                                builder.append("?");
1224                                for (Iterator<Map.Entry<String, Object>> iter =
1225                                                                theParameters.entrySet().iterator();
1226                                                iter.hasNext(); ) {
1227                                        Map.Entry<String, Object> nextEntry = iter.next();
1228                                        builder.append(UrlUtil.escapeUrlParam(nextEntry.getKey()));
1229                                        builder.append("=");
1230                                        builder.append(UrlUtil.escapeUrlParam(
1231                                                        defaultIfNull(nextEntry.getValue(), "").toString()));
1232                                        if (iter.hasNext()) {
1233                                                builder.append("&");
1234                                        }
1235                                }
1236                        }
1237
1238                        return builder.toString();
1239                }
1240        }
1241
1242        private Schema<?> toSchema(Enumerations.SearchParamType type) {
1243                if (type == null) {
1244                        return new StringSchema();
1245                }
1246                switch (type) {
1247                        case NUMBER:
1248                                return new NumberSchema();
1249                        case DATE:
1250                                Schema<?> dateSchema = new Schema<>();
1251                                dateSchema.anyOf(ImmutableList.of(new DateTimeSchema(), new DateSchema()));
1252                                return dateSchema;
1253                        case QUANTITY:
1254                                Schema<?> quantitySchema = new Schema<>();
1255                                quantitySchema.anyOf(ImmutableList.of(new StringSchema(), new NumberSchema()));
1256                                return quantitySchema;
1257                        case STRING:
1258                        case TOKEN:
1259                        case REFERENCE:
1260                        case COMPOSITE:
1261                        case URI:
1262                        case SPECIAL:
1263                        case NULL:
1264                        default:
1265                                return new StringSchema();
1266                }
1267        }
1268}