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                terser.addElement(retVal, "publisher", myPublisher);
210                terser.addElement(retVal, "date", conformanceDate(configuration));
211                terser.addElement(
212                                retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString());
213
214                ServletContext servletContext = (ServletContext)
215                                (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE));
216                String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest);
217                terser.addElement(retVal, "implementation.url", serverBase);
218                terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription());
219                terser.addElement(retVal, "kind", "instance");
220                if (myServer != null && isNotBlank(myServer.getCopyright())) {
221                        terser.addElement(retVal, "copyright", myServer.getCopyright());
222                }
223                terser.addElement(retVal, "software.name", configuration.getServerName());
224                terser.addElement(retVal, "software.version", configuration.getServerVersion());
225                if (myContext.isFormatXmlSupported()) {
226                        terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
227                        terser.addElement(retVal, "format", Constants.FORMAT_XML);
228                }
229                if (myContext.isFormatJsonSupported()) {
230                        terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW);
231                        terser.addElement(retVal, "format", Constants.FORMAT_JSON);
232                }
233                if (myContext.isFormatRdfSupported()) {
234                        terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE);
235                        terser.addElement(retVal, "format", Constants.FORMAT_TURTLE);
236                }
237                terser.addElement(retVal, "status", "active");
238
239                IBase rest = terser.addElement(retVal, "rest");
240                terser.addElement(rest, "mode", "server");
241
242                Set<String> systemOps = new HashSet<>();
243
244                Map<String, List<BaseMethodBinding>> resourceToMethods = configuration.collectMethodBindings();
245                Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype =
246                                configuration.getNameToSharedSupertype();
247                List<BaseMethodBinding> globalMethodBindings = configuration.getGlobalBindings();
248
249                TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create();
250                TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create();
251                for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) {
252                        String resourceName = nextEntry.getKey();
253                        for (BaseMethodBinding nextMethod : nextEntry.getValue()) {
254                                if (nextMethod instanceof SearchMethodBinding) {
255                                        resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes());
256                                        resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes());
257                                }
258                        }
259                }
260
261                for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) {
262
263                        Set<String> operationNames = new HashSet<>();
264                        String resourceName = nextEntry.getKey();
265                        if (nextEntry.getKey().isEmpty() == false) {
266                                Set<String> resourceOps = new HashSet<>();
267                                IBase resource = terser.addElement(rest, "resource");
268
269                                postProcessRestResource(terser, resource, resourceName);
270
271                                RuntimeResourceDefinition def;
272                                FhirContext context = configuration.getFhirContext();
273                                if (resourceNameToSharedSupertype.containsKey(resourceName)) {
274                                        def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName));
275                                } else {
276                                        def = context.getResourceDefinition(resourceName);
277                                }
278                                terser.addElement(resource, "type", def.getName());
279                                terser.addElement(resource, "profile", def.getResourceProfile(serverBase));
280
281                                for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) {
282                                        RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType();
283                                        if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) {
284                                                String resOp;
285                                                resOp = resOpCode.getCode();
286                                                if (resourceOps.contains(resOp) == false) {
287                                                        resourceOps.add(resOp);
288                                                        IBase interaction = terser.addElement(resource, "interaction");
289                                                        terser.addElement(interaction, "code", resOp);
290                                                }
291                                                if (RestOperationTypeEnum.VREAD.equals(resOpCode)) {
292                                                        // vread implies read
293                                                        resOp = "read";
294                                                        if (resourceOps.contains(resOp) == false) {
295                                                                resourceOps.add(resOp);
296                                                                IBase interaction = terser.addElement(resource, "interaction");
297                                                                terser.addElement(interaction, "code", resOp);
298                                                        }
299                                                }
300                                        }
301
302                                        if (nextMethodBinding.isSupportsConditional()) {
303                                                switch (resOpCode) {
304                                                        case CREATE:
305                                                                terser.setElement(resource, "conditionalCreate", "true");
306                                                                break;
307                                                        case DELETE:
308                                                                if (nextMethodBinding.isSupportsConditionalMultiple()) {
309                                                                        terser.setElement(resource, "conditionalDelete", "multiple");
310                                                                } else {
311                                                                        terser.setElement(resource, "conditionalDelete", "single");
312                                                                }
313                                                                break;
314                                                        case UPDATE:
315                                                                terser.setElement(resource, "conditionalUpdate", "true");
316                                                                break;
317                                                        case HISTORY_INSTANCE:
318                                                        case HISTORY_SYSTEM:
319                                                        case HISTORY_TYPE:
320                                                        case READ:
321                                                        case SEARCH_SYSTEM:
322                                                        case SEARCH_TYPE:
323                                                        case TRANSACTION:
324                                                        case VALIDATE:
325                                                        case VREAD:
326                                                        case METADATA:
327                                                        case META_ADD:
328                                                        case META:
329                                                        case META_DELETE:
330                                                        case PATCH:
331                                                        case BATCH:
332                                                        case ADD_TAGS:
333                                                        case DELETE_TAGS:
334                                                        case GET_TAGS:
335                                                        case GET_PAGE:
336                                                        case GRAPHQL_REQUEST:
337                                                        case EXTENDED_OPERATION_SERVER:
338                                                        case EXTENDED_OPERATION_TYPE:
339                                                        case EXTENDED_OPERATION_INSTANCE:
340                                                        default:
341                                                                break;
342                                                }
343                                        }
344
345                                        checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
346
347                                        // Resource Operations
348                                        if (nextMethodBinding instanceof SearchMethodBinding) {
349                                                addSearchMethodIfSearchIsNamedQuery(
350                                                                theRequestDetails, bindings, terser, operationNames, resource, (SearchMethodBinding)
351                                                                                nextMethodBinding);
352                                        } else if (nextMethodBinding instanceof OperationMethodBinding) {
353                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
354                                                String opName = bindings.getOperationBindingToId().get(methodBinding);
355                                                // Only add each operation (by name) once
356                                                if (operationNames.add(opName)) {
357                                                        IBase operation = terser.addElement(resource, "operation");
358                                                        populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
359                                                }
360                                        }
361                                }
362
363                                // Find any global operations (Operations defines at the system level but with the
364                                // global flag set to true, meaning they apply to all resource types)
365                                if (globalMethodBindings != null) {
366                                        Set<String> globalOperationNames = new HashSet<>();
367                                        for (BaseMethodBinding next : globalMethodBindings) {
368                                                if (next instanceof OperationMethodBinding) {
369                                                        OperationMethodBinding methodBinding = (OperationMethodBinding) next;
370                                                        if (methodBinding.isGlobalMethod()) {
371                                                                if (methodBinding.isCanOperateAtInstanceLevel()
372                                                                                || methodBinding.isCanOperateAtTypeLevel()) {
373                                                                        String opName =
374                                                                                        bindings.getOperationBindingToId().get(methodBinding);
375                                                                        // Only add each operation (by name) once
376                                                                        if (globalOperationNames.add(opName)) {
377                                                                                IBase operation = terser.addElement(resource, "operation");
378                                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
379                                                                        }
380                                                                }
381                                                        }
382                                                }
383                                        }
384                                }
385
386                                ISearchParamRegistry serverConfiguration;
387                                if (myServerConfiguration != null) {
388                                        serverConfiguration = myServerConfiguration;
389                                } else {
390                                        serverConfiguration = myServer.createConfiguration();
391                                }
392
393                                /*
394                                 * If we have an explicit registry (which will be the case in the JPA server) we use it as priority,
395                                 * but also fill in any gaps using params from the server itself. This makes sure we include
396                                 * global params like _lastUpdated
397                                 */
398                                ResourceSearchParams searchParams;
399                                ISearchParamRegistry searchParamRegistry;
400                                ResourceSearchParams serverConfigurationActiveSearchParams =
401                                                serverConfiguration.getActiveSearchParams(resourceName);
402                                if (mySearchParamRegistry != null) {
403                                        searchParamRegistry = mySearchParamRegistry;
404                                        searchParams = mySearchParamRegistry
405                                                        .getActiveSearchParams(resourceName)
406                                                        .makeCopy();
407                                        for (String nextBuiltInSpName : serverConfigurationActiveSearchParams.getSearchParamNames()) {
408                                                if (nextBuiltInSpName.startsWith("_")
409                                                                && !searchParams.containsParamName(nextBuiltInSpName)
410                                                                && searchParamEnabled(nextBuiltInSpName)) {
411                                                        searchParams.put(
412                                                                        nextBuiltInSpName, serverConfigurationActiveSearchParams.get(nextBuiltInSpName));
413                                                }
414                                        }
415                                } else {
416                                        searchParamRegistry = serverConfiguration;
417                                        searchParams = serverConfigurationActiveSearchParams;
418                                }
419
420                                for (RuntimeSearchParam next : searchParams.values()) {
421                                        IBase searchParam = terser.addElement(resource, "searchParam");
422                                        terser.addElement(searchParam, "name", next.getName());
423                                        terser.addElement(searchParam, "type", next.getParamType().getCode());
424                                        if (isNotBlank(next.getDescription())) {
425                                                terser.addElement(searchParam, "documentation", next.getDescription());
426                                        }
427
428                                        String spUri = next.getUri();
429
430                                        if (isNotBlank(spUri)) {
431                                                terser.addElement(searchParam, "definition", spUri);
432                                        }
433                                }
434
435                                // Add Include to CapabilityStatement.rest.resource
436                                NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName);
437                                if (resourceIncludes.isEmpty()) {
438                                        List<String> includes = searchParams.values().stream()
439                                                        .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
440                                                        .map(t -> resourceName + ":" + t.getName())
441                                                        .sorted()
442                                                        .collect(Collectors.toList());
443                                        terser.addElement(resource, "searchInclude", "*");
444                                        for (String nextInclude : includes) {
445                                                terser.addElement(resource, "searchInclude", nextInclude);
446                                        }
447                                } else {
448                                        for (String resourceInclude : resourceIncludes) {
449                                                terser.addElement(resource, "searchInclude", resourceInclude);
450                                        }
451                                }
452
453                                // Add RevInclude to CapabilityStatement.rest.resource
454                                if (myRestResourceRevIncludesEnabled) {
455                                        NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName);
456                                        if (resourceRevIncludes.isEmpty()) {
457                                                TreeSet<String> revIncludes = new TreeSet<>();
458                                                for (String nextResourceName : resourceToMethods.keySet()) {
459                                                        if (isBlank(nextResourceName)) {
460                                                                continue;
461                                                        }
462
463                                                        for (RuntimeSearchParam t : searchParamRegistry
464                                                                        .getActiveSearchParams(nextResourceName)
465                                                                        .values()) {
466                                                                if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
467                                                                        if (isNotBlank(t.getName())) {
468                                                                                boolean appropriateTarget = false;
469                                                                                if (t.getTargets().contains(resourceName)
470                                                                                                || t.getTargets().isEmpty()) {
471                                                                                        appropriateTarget = true;
472                                                                                }
473
474                                                                                if (appropriateTarget) {
475                                                                                        revIncludes.add(nextResourceName + ":" + t.getName());
476                                                                                }
477                                                                        }
478                                                                }
479                                                        }
480                                                }
481                                                for (String nextInclude : revIncludes) {
482                                                        terser.addElement(resource, "searchRevInclude", nextInclude);
483                                                }
484                                        } else {
485                                                for (String resourceInclude : resourceRevIncludes) {
486                                                        terser.addElement(resource, "searchRevInclude", resourceInclude);
487                                                }
488                                        }
489                                }
490
491                                // Add SupportedProfile to CapabilityStatement.rest.resource
492                                for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) {
493                                        terser.addElement(resource, "supportedProfile", supportedProfile);
494                                }
495
496                        } else {
497                                for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) {
498                                        checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
499                                        if (nextMethodBinding instanceof OperationMethodBinding) {
500                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
501                                                if (!methodBinding.isGlobalMethod()) {
502                                                        String opName = bindings.getOperationBindingToId().get(methodBinding);
503                                                        if (operationNames.add(opName)) {
504                                                                ourLog.debug("Found bound operation: {}", opName);
505                                                                IBase operation = terser.addElement(rest, "operation");
506                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
507                                                        }
508                                                }
509                                        } else if (nextMethodBinding instanceof SearchMethodBinding) {
510                                                addSearchMethodIfSearchIsNamedQuery(
511                                                                theRequestDetails, bindings, terser, operationNames, rest, (SearchMethodBinding)
512                                                                                nextMethodBinding);
513                                        }
514                                }
515                        }
516                }
517
518                // Find any global operations (Operations defines at the system level but with the
519                // global flag set to true, meaning they apply to all resource types)
520                if (globalMethodBindings != null) {
521                        Set<String> globalOperationNames = new HashSet<>();
522                        for (BaseMethodBinding next : globalMethodBindings) {
523                                if (next instanceof OperationMethodBinding) {
524                                        OperationMethodBinding methodBinding = (OperationMethodBinding) next;
525                                        if (methodBinding.isGlobalMethod()) {
526                                                if (methodBinding.isCanOperateAtServerLevel()) {
527                                                        String opName = bindings.getOperationBindingToId().get(methodBinding);
528                                                        // Only add each operation (by name) once
529                                                        if (globalOperationNames.add(opName)) {
530                                                                IBase operation = terser.addElement(rest, "operation");
531                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
532                                                        }
533                                                }
534                                        }
535                                }
536                        }
537                }
538
539                maybeAddBulkDataDeclarationToConformingToIg(terser, retVal, configuration.getServerBindings());
540
541                postProcessRest(terser, rest);
542                postProcess(terser, retVal);
543
544                return retVal;
545        }
546
547        private void maybeAddBulkDataDeclarationToConformingToIg(
548                        FhirTerser theTerser, IBaseConformance theBaseConformance, List<BaseMethodBinding> theServerBindings) {
549                boolean bulkExportEnabled = theServerBindings.stream()
550                                .filter(OperationMethodBinding.class::isInstance)
551                                .map(OperationMethodBinding.class::cast)
552                                .map(OperationMethodBinding::getName)
553                                .anyMatch(ProviderConstants.OPERATION_EXPORT::equals);
554
555                if (bulkExportEnabled) {
556                        theTerser.addElement(theBaseConformance, "instantiates", Constants.BULK_DATA_ACCESS_IG_URL);
557                }
558        }
559
560        /**
561         *
562         * @param theSearchParam
563         * @return true if theSearchParam is enabled on this server
564         */
565        protected boolean searchParamEnabled(String theSearchParam) {
566                return true;
567        }
568
569        private void addSearchMethodIfSearchIsNamedQuery(
570                        RequestDetails theRequestDetails,
571                        Bindings theBindings,
572                        FhirTerser theTerser,
573                        Set<String> theOperationNamesAlreadyAdded,
574                        IBase theElementToAddTo,
575                        SearchMethodBinding theSearchMethodBinding) {
576                if (theSearchMethodBinding.getQueryName() != null) {
577                        String queryName = theBindings.getNamedSearchMethodBindingToName().get(theSearchMethodBinding);
578                        if (theOperationNamesAlreadyAdded.add(queryName)) {
579                                IBase operation = theTerser.addElement(theElementToAddTo, "operation");
580                                theTerser.addElement(operation, "name", theSearchMethodBinding.getQueryName());
581                                theTerser.addElement(operation, "definition", (createOperationUrl(theRequestDetails, queryName)));
582                        }
583                }
584        }
585
586        private void populateOperation(
587                        RequestDetails theRequestDetails,
588                        FhirTerser theTerser,
589                        OperationMethodBinding theMethodBinding,
590                        String theOpName,
591                        IBase theOperation) {
592                String operationName = theMethodBinding.getName().substring(1);
593                theTerser.addElement(theOperation, "name", operationName);
594                String operationCanonicalUrl = theMethodBinding.getCanonicalUrl();
595                if (isNotBlank(operationCanonicalUrl)) {
596                        theTerser.addElement(theOperation, "definition", operationCanonicalUrl);
597                        operationCanonicalUrlToId.put(operationCanonicalUrl, theOpName);
598                } else {
599                        theTerser.addElement(theOperation, "definition", createOperationUrl(theRequestDetails, theOpName));
600                }
601                if (isNotBlank(theMethodBinding.getDescription())) {
602                        theTerser.addElement(theOperation, "documentation", theMethodBinding.getDescription());
603                }
604        }
605
606        @Nonnull
607        private String createOperationUrl(RequestDetails theRequestDetails, String theOpName) {
608                return getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + theOpName;
609        }
610
611        private TreeMultimap<String, String> getSupportedProfileMultimap(FhirTerser terser) {
612                TreeMultimap<String, String> resourceTypeToSupportedProfiles = TreeMultimap.create();
613                if (myValidationSupport != null) {
614                        List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions();
615                        if (allStructureDefinitions != null) {
616                                for (IBaseResource next : allStructureDefinitions) {
617                                        String kind = terser.getSinglePrimitiveValueOrNull(next, "kind");
618                                        String url = terser.getSinglePrimitiveValueOrNull(next, "url");
619                                        String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition"));
620                                        if ("resource".equals(kind) && isNotBlank(url)) {
621
622                                                // Don't include the base resource definitions in the supported profile list - This isn't
623                                                // helpful
624                                                if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource")
625                                                                || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) {
626                                                        continue;
627                                                }
628
629                                                String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path");
630                                                if (isBlank(resourceType)) {
631                                                        resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path");
632                                                }
633
634                                                if (isNotBlank(resourceType)) {
635                                                        resourceTypeToSupportedProfiles.put(resourceType, url);
636                                                }
637                                        }
638                                }
639                        }
640                }
641                return resourceTypeToSupportedProfiles;
642        }
643
644        /**
645         * Subclasses may override
646         */
647        protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) {
648                // nothing
649        }
650
651        /**
652         * Subclasses may override
653         */
654        protected void postProcessRest(FhirTerser theTerser, IBase theRest) {
655                // nothing
656        }
657
658        /**
659         * Subclasses may override
660         */
661        protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) {
662                // nothing
663        }
664
665        protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) {
666                if (theRequestDetails == null) {
667                        return "";
668                }
669                return theRequestDetails.getServerBaseForRequest() + "/";
670        }
671
672        @Override
673        @Read(typeName = "OperationDefinition")
674        public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) {
675                if (theId == null || theId.hasIdPart() == false) {
676                        throw new ResourceNotFoundException(Msg.code(2245) + theId);
677                }
678                RestfulServerConfiguration configuration = getServerConfiguration();
679                Bindings bindings = configuration.provideBindings();
680                String operationId = getOperationId(theId);
681                List<OperationMethodBinding> operationBindings =
682                                bindings.getOperationIdToBindings().get(operationId);
683                if (operationBindings != null && !operationBindings.isEmpty()) {
684                        return readOperationDefinitionForOperation(theRequestDetails, bindings, operationBindings);
685                }
686
687                List<SearchMethodBinding> searchBindings =
688                                bindings.getSearchNameToBindings().get(theId.getIdPart());
689                if (searchBindings != null && !searchBindings.isEmpty()) {
690                        return readOperationDefinitionForNamedSearch(searchBindings);
691                }
692                throw new ResourceNotFoundException(Msg.code(2249) + theId);
693        }
694
695        private String getOperationId(IIdType theId) {
696                if (operationCanonicalUrlToId.get(theId.getValue()) != null) {
697                        return operationCanonicalUrlToId.get(theId.getValue());
698                }
699                return theId.getIdPart();
700        }
701
702        private IBaseResource readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) {
703                IBaseResource op =
704                                myContext.getResourceDefinition("OperationDefinition").newInstance();
705                FhirTerser terser = myContext.newTerser();
706
707                terser.addElement(op, "status", "active");
708                terser.addElement(op, "kind", "query");
709                terser.addElement(op, "affectsState", "false");
710
711                terser.addElement(op, "instance", "false");
712
713                Set<String> inParams = new HashSet<>();
714
715                String operationCode = null;
716                for (SearchMethodBinding binding : bindings) {
717                        if (isNotBlank(binding.getDescription())) {
718                                terser.addElement(op, "description", binding.getDescription());
719                        }
720                        if (isBlank(binding.getResourceProviderResourceName())) {
721                                terser.addElement(op, "system", "true");
722                                terser.addElement(op, "type", "false");
723                        } else {
724                                terser.addElement(op, "system", "false");
725                                terser.addElement(op, "type", "true");
726                                terser.addElement(op, "resource", binding.getResourceProviderResourceName());
727                        }
728
729                        if (operationCode == null) {
730                                operationCode = binding.getQueryName();
731                        }
732
733                        for (IParameter nextParamUntyped : binding.getParameters()) {
734                                if (nextParamUntyped instanceof SearchParameter) {
735                                        SearchParameter nextParam = (SearchParameter) nextParamUntyped;
736                                        if (!inParams.add(nextParam.getName())) {
737                                                continue;
738                                        }
739
740                                        IBase param = terser.addElement(op, "parameter");
741                                        terser.addElement(param, "use", "in");
742                                        terser.addElement(param, "type", "string");
743                                        terser.addElement(
744                                                        param, "searchType", nextParam.getParamType().getCode());
745                                        terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0");
746                                        terser.addElement(param, "max", "1");
747                                        terser.addElement(param, "name", nextParam.getName());
748                                }
749                        }
750                }
751
752                terser.addElement(op, "code", operationCode);
753
754                String operationName = WordUtils.capitalize(operationCode);
755                terser.addElement(op, "name", operationName);
756
757                return op;
758        }
759
760        private IBaseResource readOperationDefinitionForOperation(
761                        RequestDetails theRequestDetails,
762                        Bindings theBindings,
763                        List<OperationMethodBinding> theOperationMethodBindings) {
764                IBaseResource op =
765                                myContext.getResourceDefinition("OperationDefinition").newInstance();
766                FhirTerser terser = myContext.newTerser();
767
768                terser.addElement(op, "status", "active");
769                terser.addElement(op, "kind", "operation");
770
771                boolean systemLevel = false;
772                boolean typeLevel = false;
773                boolean instanceLevel = false;
774                boolean affectsState = false;
775                String description = null;
776                String title = null;
777                String code = null;
778                String url = null;
779
780                Set<String> resourceNames = new TreeSet<>();
781                Map<String, IBase> inParams = new HashMap<>();
782                Map<String, IBase> outParams = new HashMap<>();
783
784                for (OperationMethodBinding operationMethodBinding : theOperationMethodBindings) {
785                        if (isNotBlank(operationMethodBinding.getDescription()) && isBlank(description)) {
786                                description = operationMethodBinding.getDescription();
787                        }
788                        if (isNotBlank(operationMethodBinding.getShortDescription()) && isBlank(title)) {
789                                title = operationMethodBinding.getShortDescription();
790                        }
791                        if (operationMethodBinding.isCanOperateAtInstanceLevel()) {
792                                instanceLevel = true;
793                        }
794                        if (operationMethodBinding.isCanOperateAtServerLevel()) {
795                                systemLevel = true;
796                        }
797                        if (operationMethodBinding.isCanOperateAtTypeLevel()) {
798                                typeLevel = true;
799                        }
800                        if (!operationMethodBinding.isIdempotent()) {
801                                affectsState |= true;
802                        }
803
804                        code = operationMethodBinding.getName().substring(1);
805
806                        if (isNotBlank(operationMethodBinding.getResourceName())) {
807                                resourceNames.add(operationMethodBinding.getResourceName());
808                        }
809
810                        if (isBlank(url)) {
811                                url = theBindings.getOperationBindingToId().get(operationMethodBinding);
812                                if (isNotBlank(url)) {
813                                        url = createOperationUrl(theRequestDetails, url);
814                                }
815                        }
816
817                        for (IParameter nextParamUntyped : operationMethodBinding.getParameters()) {
818                                if (nextParamUntyped instanceof OperationParameter) {
819                                        OperationParameter nextParam = (OperationParameter) nextParamUntyped;
820
821                                        IBase param = inParams.get(nextParam.getName());
822                                        if (param == null) {
823                                                param = terser.addElement(op, "parameter");
824                                                inParams.put(nextParam.getName(), param);
825                                        }
826
827                                        IBase existingParam = inParams.get(nextParam.getName());
828                                        if (isNotBlank(nextParam.getDescription())
829                                                        && terser.getValues(existingParam, "documentation").isEmpty()) {
830                                                terser.addElement(existingParam, "documentation", nextParam.getDescription());
831                                        }
832
833                                        if (nextParam.getParamType() != null) {
834                                                String existingType = terser.getSinglePrimitiveValueOrNull(existingParam, "type");
835                                                if (!nextParam.getParamType().equals(existingType)) {
836                                                        if (existingType == null) {
837                                                                terser.setElement(existingParam, "type", nextParam.getParamType());
838                                                        } else {
839                                                                terser.setElement(existingParam, "type", "Resource");
840                                                        }
841                                                }
842                                        }
843
844                                        terser.setElement(param, "use", "in");
845                                        if (nextParam.getSearchParamType() != null) {
846                                                terser.setElement(param, "searchType", nextParam.getSearchParamType());
847                                        }
848                                        terser.setElement(param, "min", Integer.toString(nextParam.getMin()));
849                                        terser.setElement(
850                                                        param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())));
851                                        terser.setElement(param, "name", nextParam.getName());
852
853                                        List<IBaseExtension<?, ?>> existingExampleExtensions = ExtensionUtil.getExtensionsByUrl(
854                                                        (IBaseHasExtensions) param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE);
855                                        Set<String> existingExamples = existingExampleExtensions.stream()
856                                                        .map(t -> t.getValue())
857                                                        .filter(t -> t != null)
858                                                        .map(t -> (IPrimitiveType<?>) t)
859                                                        .map(t -> t.getValueAsString())
860                                                        .collect(Collectors.toSet());
861                                        for (String nextExample : nextParam.getExampleValues()) {
862                                                if (!existingExamples.contains(nextExample)) {
863                                                        ExtensionUtil.addExtension(
864                                                                        myContext,
865                                                                        param,
866                                                                        HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE,
867                                                                        "string",
868                                                                        nextExample);
869                                                }
870                                        }
871                                }
872                        }
873
874                        for (ReturnType nextParam : operationMethodBinding.getReturnParams()) {
875                                if (outParams.containsKey(nextParam.getName())) {
876                                        continue;
877                                }
878
879                                IBase param = terser.addElement(op, "parameter");
880                                outParams.put(nextParam.getName(), param);
881
882                                terser.addElement(param, "use", "out");
883                                if (nextParam.getType() != null) {
884                                        terser.addElement(param, "type", nextParam.getType());
885                                }
886                                terser.addElement(param, "min", Integer.toString(nextParam.getMin()));
887                                terser.addElement(
888                                                param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())));
889                                terser.addElement(param, "name", nextParam.getName());
890                        }
891                }
892                String name = WordUtils.capitalize(code);
893
894                terser.addElements(op, "resource", resourceNames);
895                terser.addElement(op, "name", name);
896                terser.addElement(op, "url", url);
897                terser.addElement(op, "code", code);
898                terser.addElement(op, "description", description);
899                terser.addElement(op, "title", title);
900                terser.addElement(op, "affectsState", Boolean.toString(affectsState));
901                terser.addElement(op, "system", Boolean.toString(systemLevel));
902                terser.addElement(op, "type", Boolean.toString(typeLevel));
903                terser.addElement(op, "instance", Boolean.toString(instanceLevel));
904
905                return op;
906        }
907
908        @Override
909        public void setRestfulServer(RestfulServer theRestfulServer) {
910                // ignore
911        }
912
913        public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) {
914                myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled;
915        }
916}