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