001/*
002 * #%L
003 * HAPI FHIR - Server Framework
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.server.provider;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.context.support.IValidationSupport;
026import ca.uhn.fhir.i18n.Msg;
027import ca.uhn.fhir.model.primitive.InstantDt;
028import ca.uhn.fhir.parser.DataFormatException;
029import ca.uhn.fhir.rest.annotation.IdParam;
030import ca.uhn.fhir.rest.annotation.Metadata;
031import ca.uhn.fhir.rest.annotation.Read;
032import ca.uhn.fhir.rest.api.Constants;
033import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
034import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
035import ca.uhn.fhir.rest.api.server.RequestDetails;
036import ca.uhn.fhir.rest.server.Bindings;
037import ca.uhn.fhir.rest.server.IServerConformanceProvider;
038import ca.uhn.fhir.rest.server.RestfulServer;
039import ca.uhn.fhir.rest.server.RestfulServerConfiguration;
040import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
041import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
042import ca.uhn.fhir.rest.server.method.IParameter;
043import ca.uhn.fhir.rest.server.method.OperationMethodBinding;
044import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType;
045import ca.uhn.fhir.rest.server.method.OperationParameter;
046import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
047import ca.uhn.fhir.rest.server.method.SearchParameter;
048import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
049import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
050import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
051import ca.uhn.fhir.util.ExtensionUtil;
052import ca.uhn.fhir.util.FhirTerser;
053import ca.uhn.fhir.util.HapiExtensions;
054import com.google.common.collect.TreeMultimap;
055import jakarta.annotation.Nonnull;
056import jakarta.servlet.ServletContext;
057import jakarta.servlet.http.HttpServletRequest;
058import org.apache.commons.text.WordUtils;
059import org.hl7.fhir.instance.model.api.IBase;
060import org.hl7.fhir.instance.model.api.IBaseConformance;
061import org.hl7.fhir.instance.model.api.IBaseExtension;
062import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
063import org.hl7.fhir.instance.model.api.IBaseResource;
064import org.hl7.fhir.instance.model.api.IIdType;
065import org.hl7.fhir.instance.model.api.IPrimitiveType;
066import org.slf4j.Logger;
067import org.slf4j.LoggerFactory;
068
069import java.util.Date;
070import java.util.HashMap;
071import java.util.HashSet;
072import java.util.List;
073import java.util.Map;
074import java.util.Map.Entry;
075import java.util.NavigableSet;
076import java.util.Set;
077import java.util.TreeSet;
078import java.util.UUID;
079import java.util.stream.Collectors;
080
081import static org.apache.commons.lang3.StringUtils.defaultString;
082import static org.apache.commons.lang3.StringUtils.isBlank;
083import static org.apache.commons.lang3.StringUtils.isNotBlank;
084
085/**
086 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation
087 * <p>
088 * This class is version independent, but will only work on servers supporting FHIR R4+ (as this was
089 * the first FHIR release where CapabilityStatement was a normative resource)
090 */
091public class ServerCapabilityStatementProvider implements IServerConformanceProvider<IBaseConformance> {
092
093        public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true;
094        private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class);
095        private final FhirContext myContext;
096        private final RestfulServer myServer;
097        private final ISearchParamRegistry mySearchParamRegistry;
098        private final RestfulServerConfiguration myServerConfiguration;
099        private final IValidationSupport myValidationSupport;
100        private String myPublisher = "Not provided";
101        private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED;
102        private HashMap<String, String> operationCanonicalUrlToId = new HashMap<>();
103        /**
104         * Constructor
105         */
106        public ServerCapabilityStatementProvider(RestfulServer theServer) {
107                myServer = theServer;
108                myContext = theServer.getFhirContext();
109                mySearchParamRegistry = null;
110                myServerConfiguration = null;
111                myValidationSupport = null;
112        }
113
114        /**
115         * Constructor
116         */
117        public ServerCapabilityStatementProvider(
118                        FhirContext theContext, RestfulServerConfiguration theServerConfiguration) {
119                myContext = theContext;
120                myServerConfiguration = theServerConfiguration;
121                mySearchParamRegistry = null;
122                myServer = null;
123                myValidationSupport = null;
124        }
125
126        /**
127         * Constructor
128         */
129        public ServerCapabilityStatementProvider(
130                        RestfulServer theRestfulServer,
131                        ISearchParamRegistry theSearchParamRegistry,
132                        IValidationSupport theValidationSupport) {
133                myContext = theRestfulServer.getFhirContext();
134                mySearchParamRegistry = theSearchParamRegistry;
135                myServer = theRestfulServer;
136                myServerConfiguration = null;
137                myValidationSupport = theValidationSupport;
138        }
139
140        private void checkBindingForSystemOps(
141                        FhirTerser theTerser, IBase theRest, Set<String> theSystemOps, BaseMethodBinding theMethodBinding) {
142                RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType();
143                if (restOperationType.isSystemLevel()) {
144                        String sysOp = restOperationType.getCode();
145                        if (theSystemOps.contains(sysOp) == false) {
146                                theSystemOps.add(sysOp);
147                                IBase interaction = theTerser.addElement(theRest, "interaction");
148                                theTerser.addElement(interaction, "code", sysOp);
149                        }
150                }
151        }
152
153        private String conformanceDate(RestfulServerConfiguration theServerConfiguration) {
154                IPrimitiveType<Date> buildDate = theServerConfiguration.getConformanceDate();
155                if (buildDate != null && buildDate.getValue() != null) {
156                        try {
157                                return buildDate.getValueAsString();
158                        } catch (DataFormatException e) {
159                                // fall through
160                        }
161                }
162                return InstantDt.withCurrentTime().getValueAsString();
163        }
164
165        private RestfulServerConfiguration getServerConfiguration() {
166                if (myServer != null) {
167                        return myServer.createConfiguration();
168                }
169                return myServerConfiguration;
170        }
171
172        /**
173         * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The
174         * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
175         */
176        public String getPublisher() {
177                return myPublisher;
178        }
179
180        /**
181         * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The
182         * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
183         */
184        public void setPublisher(String thePublisher) {
185                myPublisher = thePublisher;
186        }
187
188        @Override
189        @Metadata
190        public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
191
192                HttpServletRequest servletRequest = null;
193                if (theRequestDetails instanceof ServletRequestDetails) {
194                        servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest();
195                }
196
197                RestfulServerConfiguration configuration = getServerConfiguration();
198                Bindings bindings = configuration.provideBindings();
199
200                IBaseConformance retVal = (IBaseConformance)
201                                myContext.getResourceDefinition("CapabilityStatement").newInstance();
202
203                FhirTerser terser = myContext.newTerser();
204
205                TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser);
206
207                terser.addElement(retVal, "id", UUID.randomUUID().toString());
208                terser.addElement(retVal, "name", "RestServer");
209                IBase text = terser.addElement(retVal, "text");
210                terser.addElement(text, "status", "generated");
211                terser.addElement(
212                                text, "div", "<div xmlns=\"http://www.w3.org/1999/xhtml\">" + configuration.getServerName() + "</div>");
213                terser.addElement(retVal, "publisher", myPublisher);
214                terser.addElement(retVal, "date", conformanceDate(configuration));
215                terser.addElement(
216                                retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString());
217
218                ServletContext servletContext = (ServletContext)
219                                (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE));
220                String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest);
221                terser.addElement(retVal, "implementation.url", serverBase);
222                terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription());
223                terser.addElement(retVal, "kind", "instance");
224                if (myServer != null && isNotBlank(myServer.getCopyright())) {
225                        terser.addElement(retVal, "copyright", myServer.getCopyright());
226                }
227                terser.addElement(retVal, "software.name", configuration.getServerName());
228                terser.addElement(retVal, "software.version", configuration.getServerVersion());
229                if (myContext.isFormatXmlSupported()) {
230                        terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
231                        terser.addElement(retVal, "format", Constants.FORMAT_XML);
232                }
233                if (myContext.isFormatJsonSupported()) {
234                        terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW);
235                        terser.addElement(retVal, "format", Constants.FORMAT_JSON);
236                }
237                if (myContext.isFormatRdfSupported()) {
238                        terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE);
239                        terser.addElement(retVal, "format", Constants.FORMAT_TURTLE);
240                }
241                terser.addElement(retVal, "status", "active");
242
243                IBase rest = terser.addElement(retVal, "rest");
244                terser.addElement(rest, "mode", "server");
245
246                Set<String> systemOps = new HashSet<>();
247
248                Map<String, List<BaseMethodBinding>> resourceToMethods = configuration.collectMethodBindings();
249                Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype =
250                                configuration.getNameToSharedSupertype();
251                List<BaseMethodBinding> globalMethodBindings = configuration.getGlobalBindings();
252
253                TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create();
254                TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create();
255                for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) {
256                        String resourceName = nextEntry.getKey();
257                        for (BaseMethodBinding nextMethod : nextEntry.getValue()) {
258                                if (nextMethod instanceof SearchMethodBinding) {
259                                        resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes());
260                                        resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes());
261                                }
262                        }
263                }
264
265                for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) {
266
267                        Set<String> operationNames = new HashSet<>();
268                        String resourceName = nextEntry.getKey();
269                        if (nextEntry.getKey().isEmpty() == false) {
270                                Set<String> resourceOps = new HashSet<>();
271                                IBase resource = terser.addElement(rest, "resource");
272
273                                postProcessRestResource(terser, resource, resourceName);
274
275                                RuntimeResourceDefinition def;
276                                FhirContext context = configuration.getFhirContext();
277                                if (resourceNameToSharedSupertype.containsKey(resourceName)) {
278                                        def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName));
279                                } else {
280                                        def = context.getResourceDefinition(resourceName);
281                                }
282                                terser.addElement(resource, "type", def.getName());
283                                terser.addElement(resource, "profile", def.getResourceProfile(serverBase));
284
285                                for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) {
286                                        RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType();
287                                        if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) {
288                                                String resOp;
289                                                resOp = resOpCode.getCode();
290                                                if (resourceOps.contains(resOp) == false) {
291                                                        resourceOps.add(resOp);
292                                                        IBase interaction = terser.addElement(resource, "interaction");
293                                                        terser.addElement(interaction, "code", resOp);
294                                                }
295                                                if (RestOperationTypeEnum.VREAD.equals(resOpCode)) {
296                                                        // vread implies read
297                                                        resOp = "read";
298                                                        if (resourceOps.contains(resOp) == false) {
299                                                                resourceOps.add(resOp);
300                                                                IBase interaction = terser.addElement(resource, "interaction");
301                                                                terser.addElement(interaction, "code", resOp);
302                                                        }
303                                                }
304                                        }
305
306                                        if (nextMethodBinding.isSupportsConditional()) {
307                                                switch (resOpCode) {
308                                                        case CREATE:
309                                                                terser.setElement(resource, "conditionalCreate", "true");
310                                                                break;
311                                                        case DELETE:
312                                                                if (nextMethodBinding.isSupportsConditionalMultiple()) {
313                                                                        terser.setElement(resource, "conditionalDelete", "multiple");
314                                                                } else {
315                                                                        terser.setElement(resource, "conditionalDelete", "single");
316                                                                }
317                                                                break;
318                                                        case UPDATE:
319                                                                terser.setElement(resource, "conditionalUpdate", "true");
320                                                                break;
321                                                        case UPDATE_REWRITE_HISTORY:
322                                                        case HISTORY_INSTANCE:
323                                                        case HISTORY_SYSTEM:
324                                                        case HISTORY_TYPE:
325                                                        case READ:
326                                                        case SEARCH_SYSTEM:
327                                                        case SEARCH_TYPE:
328                                                        case TRANSACTION:
329                                                        case VALIDATE:
330                                                        case VREAD:
331                                                        case METADATA:
332                                                        case META_ADD:
333                                                        case META:
334                                                        case META_DELETE:
335                                                        case PATCH:
336                                                        case BATCH:
337                                                        case ADD_TAGS:
338                                                        case DELETE_TAGS:
339                                                        case GET_TAGS:
340                                                        case GET_PAGE:
341                                                        case GRAPHQL_REQUEST:
342                                                        case EXTENDED_OPERATION_SERVER:
343                                                        case EXTENDED_OPERATION_TYPE:
344                                                        case EXTENDED_OPERATION_INSTANCE:
345                                                        default:
346                                                                break;
347                                                }
348                                        }
349
350                                        checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
351
352                                        // Resource Operations
353                                        if (nextMethodBinding instanceof SearchMethodBinding) {
354                                                addSearchMethodIfSearchIsNamedQuery(
355                                                                theRequestDetails, bindings, terser, operationNames, resource, (SearchMethodBinding)
356                                                                                nextMethodBinding);
357                                        } else if (nextMethodBinding instanceof OperationMethodBinding) {
358                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
359                                                String opName = bindings.getOperationBindingToId().get(methodBinding);
360                                                // Only add each operation (by name) once
361                                                if (operationNames.add(opName)) {
362                                                        IBase operation = terser.addElement(resource, "operation");
363                                                        populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
364                                                }
365                                        }
366                                }
367
368                                // Find any global operations (Operations defines at the system level but with the
369                                // global flag set to true, meaning they apply to all resource types)
370                                if (globalMethodBindings != null) {
371                                        Set<String> globalOperationNames = new HashSet<>();
372                                        for (BaseMethodBinding next : globalMethodBindings) {
373                                                if (next instanceof OperationMethodBinding) {
374                                                        OperationMethodBinding methodBinding = (OperationMethodBinding) next;
375                                                        if (methodBinding.isGlobalMethod()) {
376                                                                if (methodBinding.isCanOperateAtInstanceLevel()
377                                                                                || methodBinding.isCanOperateAtTypeLevel()) {
378                                                                        String opName =
379                                                                                        bindings.getOperationBindingToId().get(methodBinding);
380                                                                        // Only add each operation (by name) once
381                                                                        if (globalOperationNames.add(opName)) {
382                                                                                IBase operation = terser.addElement(resource, "operation");
383                                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
384                                                                        }
385                                                                }
386                                                        }
387                                                }
388                                        }
389                                }
390
391                                ISearchParamRegistry serverConfiguration;
392                                if (myServerConfiguration != null) {
393                                        serverConfiguration = myServerConfiguration;
394                                } else {
395                                        serverConfiguration = myServer.createConfiguration();
396                                }
397
398                                /*
399                                 * If we have an explicit registry (which will be the case in the JPA server) we use it as priority,
400                                 * but also fill in any gaps using params from the server itself. This makes sure we include
401                                 * global params like _lastUpdated
402                                 */
403                                ResourceSearchParams searchParams;
404                                ISearchParamRegistry searchParamRegistry;
405                                ResourceSearchParams serverConfigurationActiveSearchParams = serverConfiguration.getActiveSearchParams(
406                                                resourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
407                                if (mySearchParamRegistry != null) {
408                                        searchParamRegistry = mySearchParamRegistry;
409                                        searchParams = mySearchParamRegistry
410                                                        .getActiveSearchParams(
411                                                                        resourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)
412                                                        .makeCopy();
413                                        for (String nextBuiltInSpName : serverConfigurationActiveSearchParams.getSearchParamNames()) {
414                                                if (nextBuiltInSpName.startsWith("_")
415                                                                && !searchParams.containsParamName(nextBuiltInSpName)
416                                                                && searchParamEnabled(nextBuiltInSpName)) {
417                                                        searchParams.put(
418                                                                        nextBuiltInSpName, serverConfigurationActiveSearchParams.get(nextBuiltInSpName));
419                                                }
420                                        }
421                                } else {
422                                        searchParamRegistry = serverConfiguration;
423                                        searchParams = serverConfigurationActiveSearchParams;
424                                }
425
426                                for (RuntimeSearchParam next : searchParams.values()) {
427                                        IBase searchParam = terser.addElement(resource, "searchParam");
428                                        terser.addElement(searchParam, "name", next.getName());
429                                        terser.addElement(searchParam, "type", next.getParamType().getCode());
430                                        if (isNotBlank(next.getDescription())) {
431                                                terser.addElement(searchParam, "documentation", next.getDescription());
432                                        }
433
434                                        String spUri = next.getUri();
435
436                                        if (isNotBlank(spUri)) {
437                                                terser.addElement(searchParam, "definition", spUri);
438                                        }
439                                }
440
441                                // Add Include to CapabilityStatement.rest.resource
442                                NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName);
443                                if (resourceIncludes.isEmpty()) {
444                                        List<String> includes = searchParams.values().stream()
445                                                        .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
446                                                        .map(t -> resourceName + ":" + t.getName())
447                                                        .sorted()
448                                                        .collect(Collectors.toList());
449                                        terser.addElement(resource, "searchInclude", "*");
450                                        for (String nextInclude : includes) {
451                                                terser.addElement(resource, "searchInclude", nextInclude);
452                                        }
453                                } else {
454                                        for (String resourceInclude : resourceIncludes) {
455                                                terser.addElement(resource, "searchInclude", resourceInclude);
456                                        }
457                                }
458
459                                // Add RevInclude to CapabilityStatement.rest.resource
460                                if (myRestResourceRevIncludesEnabled) {
461                                        NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName);
462                                        if (resourceRevIncludes.isEmpty()) {
463                                                TreeSet<String> revIncludes = new TreeSet<>();
464                                                for (String nextResourceName : resourceToMethods.keySet()) {
465                                                        if (isBlank(nextResourceName)) {
466                                                                continue;
467                                                        }
468
469                                                        for (RuntimeSearchParam t : searchParamRegistry
470                                                                        .getActiveSearchParams(
471                                                                                        nextResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)
472                                                                        .values()) {
473                                                                if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
474                                                                        if (isNotBlank(t.getName())) {
475                                                                                boolean appropriateTarget = false;
476                                                                                if (t.getTargets().contains(resourceName)
477                                                                                                || t.getTargets().isEmpty()) {
478                                                                                        appropriateTarget = true;
479                                                                                }
480
481                                                                                if (appropriateTarget) {
482                                                                                        revIncludes.add(nextResourceName + ":" + t.getName());
483                                                                                }
484                                                                        }
485                                                                }
486                                                        }
487                                                }
488                                                for (String nextInclude : revIncludes) {
489                                                        terser.addElement(resource, "searchRevInclude", nextInclude);
490                                                }
491                                        } else {
492                                                for (String resourceInclude : resourceRevIncludes) {
493                                                        terser.addElement(resource, "searchRevInclude", resourceInclude);
494                                                }
495                                        }
496                                }
497
498                                // Add SupportedProfile to CapabilityStatement.rest.resource
499                                for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) {
500                                        terser.addElement(resource, "supportedProfile", supportedProfile);
501                                }
502
503                        } else {
504                                for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) {
505                                        checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
506                                        if (nextMethodBinding instanceof OperationMethodBinding) {
507                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
508                                                if (!methodBinding.isGlobalMethod()) {
509                                                        String opName = bindings.getOperationBindingToId().get(methodBinding);
510                                                        if (operationNames.add(opName)) {
511                                                                ourLog.debug("Found bound operation: {}", opName);
512                                                                IBase operation = terser.addElement(rest, "operation");
513                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
514                                                        }
515                                                }
516                                        } else if (nextMethodBinding instanceof SearchMethodBinding) {
517                                                addSearchMethodIfSearchIsNamedQuery(
518                                                                theRequestDetails, bindings, terser, operationNames, rest, (SearchMethodBinding)
519                                                                                nextMethodBinding);
520                                        }
521                                }
522                        }
523                }
524
525                // Find any global operations (Operations defines at the system level but with the
526                // global flag set to true, meaning they apply to all resource types)
527                if (globalMethodBindings != null) {
528                        Set<String> globalOperationNames = new HashSet<>();
529                        for (BaseMethodBinding next : globalMethodBindings) {
530                                if (next instanceof OperationMethodBinding) {
531                                        OperationMethodBinding methodBinding = (OperationMethodBinding) next;
532                                        if (methodBinding.isGlobalMethod()) {
533                                                if (methodBinding.isCanOperateAtServerLevel()) {
534                                                        String opName = bindings.getOperationBindingToId().get(methodBinding);
535                                                        // Only add each operation (by name) once
536                                                        if (globalOperationNames.add(opName)) {
537                                                                IBase operation = terser.addElement(rest, "operation");
538                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
539                                                        }
540                                                }
541                                        }
542                                }
543                        }
544                }
545
546                maybeAddBulkDataDeclarationToConformingToIg(terser, retVal, configuration.getServerBindings());
547
548                postProcessRest(terser, rest);
549                postProcess(terser, retVal);
550
551                return retVal;
552        }
553
554        private void maybeAddBulkDataDeclarationToConformingToIg(
555                        FhirTerser theTerser, IBaseConformance theBaseConformance, List<BaseMethodBinding> theServerBindings) {
556                boolean bulkExportEnabled = theServerBindings.stream()
557                                .filter(OperationMethodBinding.class::isInstance)
558                                .map(OperationMethodBinding.class::cast)
559                                .map(OperationMethodBinding::getName)
560                                .anyMatch(ProviderConstants.OPERATION_EXPORT::equals);
561
562                if (bulkExportEnabled) {
563                        theTerser.addElement(theBaseConformance, "instantiates", Constants.BULK_DATA_ACCESS_IG_URL);
564                }
565        }
566
567        /**
568         *
569         * @param theSearchParam
570         * @return true if theSearchParam is enabled on this server
571         */
572        protected boolean searchParamEnabled(String theSearchParam) {
573                return true;
574        }
575
576        private void addSearchMethodIfSearchIsNamedQuery(
577                        RequestDetails theRequestDetails,
578                        Bindings theBindings,
579                        FhirTerser theTerser,
580                        Set<String> theOperationNamesAlreadyAdded,
581                        IBase theElementToAddTo,
582                        SearchMethodBinding theSearchMethodBinding) {
583                if (theSearchMethodBinding.getQueryName() != null) {
584                        String queryName = theBindings.getNamedSearchMethodBindingToName().get(theSearchMethodBinding);
585                        if (theOperationNamesAlreadyAdded.add(queryName)) {
586                                IBase operation = theTerser.addElement(theElementToAddTo, "operation");
587                                theTerser.addElement(operation, "name", theSearchMethodBinding.getQueryName());
588                                theTerser.addElement(operation, "definition", (createOperationUrl(theRequestDetails, queryName)));
589                        }
590                }
591        }
592
593        private void populateOperation(
594                        RequestDetails theRequestDetails,
595                        FhirTerser theTerser,
596                        OperationMethodBinding theMethodBinding,
597                        String theOpName,
598                        IBase theOperation) {
599                String operationName = theMethodBinding.getName().substring(1);
600                theTerser.addElement(theOperation, "name", operationName);
601                String operationCanonicalUrl = theMethodBinding.getCanonicalUrl();
602                if (isNotBlank(operationCanonicalUrl)) {
603                        theTerser.addElement(theOperation, "definition", operationCanonicalUrl);
604                        operationCanonicalUrlToId.put(operationCanonicalUrl, theOpName);
605                } else {
606                        theTerser.addElement(theOperation, "definition", createOperationUrl(theRequestDetails, theOpName));
607                }
608                if (isNotBlank(theMethodBinding.getDescription())) {
609                        theTerser.addElement(theOperation, "documentation", theMethodBinding.getDescription());
610                }
611        }
612
613        @Nonnull
614        private String createOperationUrl(RequestDetails theRequestDetails, String theOpName) {
615                return getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + theOpName;
616        }
617
618        private TreeMultimap<String, String> getSupportedProfileMultimap(FhirTerser terser) {
619                TreeMultimap<String, String> resourceTypeToSupportedProfiles = TreeMultimap.create();
620                if (myValidationSupport != null) {
621                        List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions();
622                        if (allStructureDefinitions != null) {
623                                for (IBaseResource next : allStructureDefinitions) {
624                                        String kind = terser.getSinglePrimitiveValueOrNull(next, "kind");
625                                        String url = terser.getSinglePrimitiveValueOrNull(next, "url");
626                                        String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition"));
627                                        if ("resource".equals(kind) && isNotBlank(url)) {
628
629                                                // Don't include the base resource definitions in the supported profile list - This isn't
630                                                // helpful
631                                                if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource")
632                                                                || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) {
633                                                        continue;
634                                                }
635
636                                                String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path");
637                                                if (isBlank(resourceType)) {
638                                                        resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path");
639                                                }
640
641                                                if (isNotBlank(resourceType)) {
642                                                        resourceTypeToSupportedProfiles.put(resourceType, url);
643                                                }
644                                        }
645                                }
646                        }
647                }
648                return resourceTypeToSupportedProfiles;
649        }
650
651        /**
652         * Subclasses may override
653         */
654        protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) {
655                // nothing
656        }
657
658        /**
659         * Subclasses may override
660         */
661        protected void postProcessRest(FhirTerser theTerser, IBase theRest) {
662                // nothing
663        }
664
665        /**
666         * Subclasses may override
667         */
668        protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) {
669                // nothing
670        }
671
672        protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) {
673                if (theRequestDetails == null) {
674                        return "";
675                }
676                return theRequestDetails.getServerBaseForRequest() + "/";
677        }
678
679        @Override
680        @Read(typeName = "OperationDefinition")
681        public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) {
682                if (theId == null || theId.hasIdPart() == false) {
683                        throw new ResourceNotFoundException(Msg.code(2245) + theId);
684                }
685                RestfulServerConfiguration configuration = getServerConfiguration();
686                Bindings bindings = configuration.provideBindings();
687                String operationId = getOperationId(theId);
688                List<OperationMethodBinding> operationBindings =
689                                bindings.getOperationIdToBindings().get(operationId);
690                if (operationBindings != null && !operationBindings.isEmpty()) {
691                        return readOperationDefinitionForOperation(theRequestDetails, bindings, operationBindings);
692                }
693
694                List<SearchMethodBinding> searchBindings =
695                                bindings.getSearchNameToBindings().get(theId.getIdPart());
696                if (searchBindings != null && !searchBindings.isEmpty()) {
697                        return readOperationDefinitionForNamedSearch(searchBindings);
698                }
699                throw new ResourceNotFoundException(Msg.code(2249) + theId);
700        }
701
702        private String getOperationId(IIdType theId) {
703                if (operationCanonicalUrlToId.get(theId.getValue()) != null) {
704                        return operationCanonicalUrlToId.get(theId.getValue());
705                }
706                return theId.getIdPart();
707        }
708
709        private IBaseResource readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) {
710                IBaseResource op =
711                                myContext.getResourceDefinition("OperationDefinition").newInstance();
712                FhirTerser terser = myContext.newTerser();
713
714                IBase text = terser.addElement(op, "text");
715                terser.addElement(text, "status", "generated");
716                terser.addElement(text, "div", "<div xmlns=\"http://www.w3.org/1999/xhtml\">Operation Named Search</div>");
717
718                terser.addElement(op, "status", "active");
719                terser.addElement(op, "kind", "query");
720                terser.addElement(op, "affectsState", "false");
721
722                terser.addElement(op, "instance", "false");
723
724                Set<String> inParams = new HashSet<>();
725
726                String operationCode = null;
727                for (SearchMethodBinding binding : bindings) {
728                        if (isNotBlank(binding.getDescription())) {
729                                terser.addElement(op, "description", binding.getDescription());
730                        }
731                        if (isBlank(binding.getResourceProviderResourceName())) {
732                                terser.addElement(op, "system", "true");
733                                terser.addElement(op, "type", "false");
734                        } else {
735                                terser.addElement(op, "system", "false");
736                                terser.addElement(op, "type", "true");
737                                terser.addElement(op, "resource", binding.getResourceProviderResourceName());
738                        }
739
740                        if (operationCode == null) {
741                                operationCode = binding.getQueryName();
742                        }
743
744                        for (IParameter nextParamUntyped : binding.getParameters()) {
745                                if (nextParamUntyped instanceof SearchParameter) {
746                                        SearchParameter nextParam = (SearchParameter) nextParamUntyped;
747                                        if (!inParams.add(nextParam.getName())) {
748                                                continue;
749                                        }
750
751                                        IBase param = terser.addElement(op, "parameter");
752                                        terser.addElement(param, "use", "in");
753                                        terser.addElement(param, "type", "string");
754                                        terser.addElement(
755                                                        param, "searchType", nextParam.getParamType().getCode());
756                                        terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0");
757                                        terser.addElement(param, "max", "1");
758                                        terser.addElement(param, "name", nextParam.getName());
759                                }
760                        }
761                }
762
763                terser.addElement(op, "code", operationCode);
764
765                String operationName = WordUtils.capitalize(operationCode);
766                terser.addElement(op, "name", operationName);
767
768                return op;
769        }
770
771        private IBaseResource readOperationDefinitionForOperation(
772                        RequestDetails theRequestDetails,
773                        Bindings theBindings,
774                        List<OperationMethodBinding> theOperationMethodBindings) {
775                IBaseResource op =
776                                myContext.getResourceDefinition("OperationDefinition").newInstance();
777                FhirTerser terser = myContext.newTerser();
778
779                IBase text = terser.addElement(op, "text");
780                terser.addElement(text, "status", "generated");
781                terser.addElement(text, "div", "<div xmlns=\"http://www.w3.org/1999/xhtml\">Operation</div>");
782
783                terser.addElement(op, "status", "active");
784                terser.addElement(op, "kind", "operation");
785
786                boolean systemLevel = false;
787                boolean typeLevel = false;
788                boolean instanceLevel = false;
789                boolean affectsState = false;
790                String description = null;
791                String title = null;
792                String code = null;
793                String url = null;
794
795                Set<String> resourceNames = new TreeSet<>();
796                Map<String, IBase> inParams = new HashMap<>();
797                Map<String, IBase> outParams = new HashMap<>();
798
799                for (OperationMethodBinding operationMethodBinding : theOperationMethodBindings) {
800                        if (isNotBlank(operationMethodBinding.getDescription()) && isBlank(description)) {
801                                description = operationMethodBinding.getDescription();
802                        }
803                        if (isNotBlank(operationMethodBinding.getShortDescription()) && isBlank(title)) {
804                                title = operationMethodBinding.getShortDescription();
805                        }
806                        if (operationMethodBinding.isCanOperateAtInstanceLevel()) {
807                                instanceLevel = true;
808                        }
809                        if (operationMethodBinding.isCanOperateAtServerLevel()) {
810                                systemLevel = true;
811                        }
812                        if (operationMethodBinding.isCanOperateAtTypeLevel()) {
813                                typeLevel = true;
814                        }
815                        if (!operationMethodBinding.isIdempotent()) {
816                                affectsState |= true;
817                        }
818
819                        code = operationMethodBinding.getName().substring(1);
820
821                        if (isNotBlank(operationMethodBinding.getResourceName())) {
822                                resourceNames.add(operationMethodBinding.getResourceName());
823                        }
824
825                        if (isBlank(url)) {
826                                url = theBindings.getOperationBindingToId().get(operationMethodBinding);
827                                if (isNotBlank(url)) {
828                                        url = createOperationUrl(theRequestDetails, url);
829                                }
830                        }
831
832                        for (IParameter nextParamUntyped : operationMethodBinding.getParameters()) {
833                                if (nextParamUntyped instanceof OperationParameter) {
834                                        OperationParameter nextParam = (OperationParameter) nextParamUntyped;
835
836                                        IBase param = inParams.get(nextParam.getName());
837                                        if (param == null) {
838                                                param = terser.addElement(op, "parameter");
839                                                inParams.put(nextParam.getName(), param);
840                                        }
841
842                                        IBase existingParam = inParams.get(nextParam.getName());
843                                        if (isNotBlank(nextParam.getDescription())
844                                                        && terser.getValues(existingParam, "documentation").isEmpty()) {
845                                                terser.addElement(existingParam, "documentation", nextParam.getDescription());
846                                        }
847
848                                        if (nextParam.getParamType() != null) {
849                                                String existingType = terser.getSinglePrimitiveValueOrNull(existingParam, "type");
850                                                if (!nextParam.getParamType().equals(existingType)) {
851                                                        if (existingType == null) {
852                                                                terser.setElement(existingParam, "type", nextParam.getParamType());
853                                                        } else {
854                                                                terser.setElement(existingParam, "type", "Resource");
855                                                        }
856                                                }
857                                        }
858
859                                        terser.setElement(param, "use", "in");
860                                        if (nextParam.getSearchParamType() != null) {
861                                                terser.setElement(param, "searchType", nextParam.getSearchParamType());
862                                        }
863                                        terser.setElement(param, "min", Integer.toString(nextParam.getMin()));
864                                        terser.setElement(
865                                                        param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())));
866                                        terser.setElement(param, "name", nextParam.getName());
867
868                                        List<IBaseExtension<?, ?>> existingExampleExtensions = ExtensionUtil.getExtensionsByUrl(
869                                                        (IBaseHasExtensions) param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE);
870                                        Set<String> existingExamples = existingExampleExtensions.stream()
871                                                        .map(t -> t.getValue())
872                                                        .filter(t -> t != null)
873                                                        .map(t -> (IPrimitiveType<?>) t)
874                                                        .map(t -> t.getValueAsString())
875                                                        .collect(Collectors.toSet());
876                                        for (String nextExample : nextParam.getExampleValues()) {
877                                                if (!existingExamples.contains(nextExample)) {
878                                                        ExtensionUtil.addExtension(
879                                                                        myContext,
880                                                                        param,
881                                                                        HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE,
882                                                                        "string",
883                                                                        nextExample);
884                                                }
885                                        }
886                                }
887                        }
888
889                        for (ReturnType nextParam : operationMethodBinding.getReturnParams()) {
890                                if (outParams.containsKey(nextParam.getName())) {
891                                        continue;
892                                }
893
894                                IBase param = terser.addElement(op, "parameter");
895                                outParams.put(nextParam.getName(), param);
896
897                                terser.addElement(param, "use", "out");
898                                if (nextParam.getType() != null) {
899                                        terser.addElement(param, "type", nextParam.getType());
900                                }
901                                terser.addElement(param, "min", Integer.toString(nextParam.getMin()));
902                                terser.addElement(
903                                                param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())));
904                                terser.addElement(param, "name", nextParam.getName());
905                        }
906                }
907                String name = WordUtils.capitalize(code);
908
909                terser.addElements(op, "resource", resourceNames);
910                terser.addElement(op, "name", name);
911                terser.addElement(op, "url", url);
912                terser.addElement(op, "code", code);
913                terser.addElement(op, "description", description);
914                terser.addElement(op, "title", title);
915                terser.addElement(op, "affectsState", Boolean.toString(affectsState));
916                terser.addElement(op, "system", Boolean.toString(systemLevel));
917                terser.addElement(op, "type", Boolean.toString(typeLevel));
918                terser.addElement(op, "instance", Boolean.toString(instanceLevel));
919
920                return op;
921        }
922
923        @Override
924        public void setRestfulServer(RestfulServer theRestfulServer) {
925                // ignore
926        }
927
928        public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) {
929                myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled;
930        }
931}