001package ca.uhn.fhir.rest.server.provider.dstu2;
002
003import ca.uhn.fhir.context.FhirVersionEnum;
004import ca.uhn.fhir.context.RuntimeResourceDefinition;
005import ca.uhn.fhir.context.RuntimeSearchParam;
006import ca.uhn.fhir.model.dstu2.resource.Conformance;
007import ca.uhn.fhir.model.dstu2.resource.Conformance.Rest;
008import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource;
009import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResourceInteraction;
010import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResourceSearchParam;
011import ca.uhn.fhir.model.dstu2.resource.OperationDefinition;
012import ca.uhn.fhir.model.dstu2.resource.OperationDefinition.Parameter;
013import ca.uhn.fhir.model.dstu2.valueset.*;
014import ca.uhn.fhir.model.primitive.DateTimeDt;
015import ca.uhn.fhir.model.primitive.IdDt;
016import ca.uhn.fhir.parser.DataFormatException;
017import ca.uhn.fhir.rest.annotation.IdParam;
018import ca.uhn.fhir.rest.annotation.Metadata;
019import ca.uhn.fhir.rest.annotation.Read;
020import ca.uhn.fhir.rest.api.Constants;
021import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
022import ca.uhn.fhir.rest.api.server.RequestDetails;
023import ca.uhn.fhir.rest.server.*;
024import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
025import ca.uhn.fhir.rest.server.method.*;
026import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType;
027import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider;
028import org.apache.commons.lang3.StringUtils;
029import org.hl7.fhir.instance.model.api.IBaseResource;
030import org.hl7.fhir.instance.model.api.IPrimitiveType;
031
032import javax.servlet.ServletContext;
033import javax.servlet.http.HttpServletRequest;
034import java.util.*;
035import java.util.Map.Entry;
036
037import static org.apache.commons.lang3.StringUtils.isBlank;
038import static org.apache.commons.lang3.StringUtils.isNotBlank;
039
040/*
041 * #%L
042 * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0)
043 * %%
044 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
045 * %%
046 * Licensed under the Apache License, Version 2.0 (the "License");
047 * you may not use this file except in compliance with the License.
048 * You may obtain a copy of the License at
049 *
050 *      http://www.apache.org/licenses/LICENSE-2.0
051 *
052 * Unless required by applicable law or agreed to in writing, software
053 * distributed under the License is distributed on an "AS IS" BASIS,
054 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
055 * See the License for the specific language governing permissions and
056 * limitations under the License.
057 * #L%
058 */
059
060/**
061 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation
062 */
063public class ServerConformanceProvider extends BaseServerCapabilityStatementProvider implements IServerConformanceProvider<Conformance> {
064
065        private String myPublisher = "Not provided";
066
067        /**
068         * No-arg constructor and setter so that the ServerConfirmanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen.
069         */
070        public ServerConformanceProvider() {
071                super();
072        }
073
074        /**
075         * Constructor
076         *
077         * @deprecated Use no-args constructor instead. Deprecated in 4.0.0
078         */
079        @Deprecated
080        public ServerConformanceProvider(RestfulServer theRestfulServer) {
081                this();
082        }
083
084        /**
085         * Constructor
086         */
087        public ServerConformanceProvider(RestfulServerConfiguration theServerConfiguration) {
088                super(theServerConfiguration);
089        }
090
091        private void checkBindingForSystemOps(Rest rest, Set<SystemRestfulInteractionEnum> systemOps, BaseMethodBinding<?> nextMethodBinding) {
092                if (nextMethodBinding.getRestOperationType() != null) {
093                        String sysOpCode = nextMethodBinding.getRestOperationType().getCode();
094                        if (sysOpCode != null) {
095                                SystemRestfulInteractionEnum sysOp = SystemRestfulInteractionEnum.VALUESET_BINDER.fromCodeString(sysOpCode);
096                                if (sysOp == null) {
097                                        return;
098                                }
099                                if (systemOps.contains(sysOp) == false) {
100                                        systemOps.add(sysOp);
101                                        rest.addInteraction().setCode(sysOp);
102                                }
103                        }
104                }
105        }
106
107        private Map<String, List<BaseMethodBinding<?>>> collectMethodBindings(RequestDetails theRequestDetails) {
108                Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
109                for (ResourceBinding next : getServerConfiguration(theRequestDetails).getResourceBindings()) {
110                        String resourceName = next.getResourceName();
111                        for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
112                                if (resourceToMethods.containsKey(resourceName) == false) {
113                                        resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
114                                }
115                                resourceToMethods.get(resourceName).add(nextMethodBinding);
116                        }
117                }
118                for (BaseMethodBinding<?> nextMethodBinding : getServerConfiguration(theRequestDetails).getServerBindings()) {
119                        String resourceName = "";
120                        if (resourceToMethods.containsKey(resourceName) == false) {
121                                resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
122                        }
123                        resourceToMethods.get(resourceName).add(nextMethodBinding);
124                }
125                return resourceToMethods;
126        }
127
128        private DateTimeDt conformanceDate(RequestDetails theRequestDetails) {
129                IPrimitiveType<Date> buildDate = getServerConfiguration(theRequestDetails).getConformanceDate();
130                if (buildDate != null && buildDate.getValue() != null) {
131                        try {
132                                return new DateTimeDt(buildDate.getValueAsString());
133                        } catch (DataFormatException e) {
134                                // fall through
135                        }
136                }
137                return DateTimeDt.withCurrentTime();
138        }
139
140        private String createOperationName(OperationMethodBinding theMethodBinding) {
141                StringBuilder retVal = new StringBuilder();
142                if (theMethodBinding.getResourceName() != null) {
143                        retVal.append(theMethodBinding.getResourceName());
144                }
145
146                retVal.append('-');
147                if (theMethodBinding.isCanOperateAtInstanceLevel()) {
148                        retVal.append('i');
149                }
150                if (theMethodBinding.isCanOperateAtServerLevel()) {
151                        retVal.append('s');
152                }
153                retVal.append('-');
154
155                // Exclude the leading $
156                retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length());
157
158                return retVal.toString();
159        }
160
161        /**
162         * 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
163         * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
164         */
165        public String getPublisher() {
166                return myPublisher;
167        }
168
169        /**
170         * 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
171         * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
172         */
173        public void setPublisher(String thePublisher) {
174                myPublisher = thePublisher;
175        }
176
177        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
178        @Override
179        @Metadata
180        public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
181                RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails);
182                Bindings bindings = serverConfiguration.provideBindings();
183
184                Conformance retVal = new Conformance();
185
186                retVal.setPublisher(myPublisher);
187                retVal.setDate(conformanceDate(theRequestDetails));
188                retVal.setFhirVersion(FhirVersionEnum.DSTU2.getFhirVersionString());
189                retVal.setAcceptUnknown(UnknownContentCodeEnum.UNKNOWN_EXTENSIONS); // TODO: make this configurable - this is a fairly big effort since the parser
190                // needs to be modified to actually allow it
191
192                ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE));
193                String serverBase = serverConfiguration.getServerAddressStrategy().determineServerBase(servletContext, theRequest);
194                retVal
195                        .getImplementation()
196                        .setUrl(serverBase)
197                        .setDescription(serverConfiguration.getImplementationDescription());
198
199                retVal.setKind(ConformanceStatementKindEnum.INSTANCE);
200                retVal.getSoftware().setName(serverConfiguration.getServerName());
201                retVal.getSoftware().setVersion(serverConfiguration.getServerVersion());
202                retVal.addFormat(Constants.CT_FHIR_XML);
203                retVal.addFormat(Constants.CT_FHIR_JSON);
204
205                Rest rest = retVal.addRest();
206                rest.setMode(RestfulConformanceModeEnum.SERVER);
207
208                Set<SystemRestfulInteractionEnum> systemOps = new HashSet<>();
209                Set<String> operationNames = new HashSet<>();
210
211                Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings(theRequestDetails);
212                for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
213
214                        if (nextEntry.getKey().isEmpty() == false) {
215                                Set<TypeRestfulInteractionEnum> resourceOps = new HashSet<>();
216                                RestResource resource = rest.addResource();
217                                String resourceName = nextEntry.getKey();
218                                RuntimeResourceDefinition def = serverConfiguration.getFhirContext().getResourceDefinition(resourceName);
219                                resource.getTypeElement().setValue(def.getName());
220                                resource.getProfile().setReference(new IdDt(def.getResourceProfile(serverBase)));
221
222                                TreeSet<String> includes = new TreeSet<>();
223
224                                // Map<String, Conformance.RestResourceSearchParam> nameToSearchParam = new HashMap<String,
225                                // Conformance.RestResourceSearchParam>();
226                                for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
227                                        if (nextMethodBinding.getRestOperationType() != null) {
228                                                String resOpCode = nextMethodBinding.getRestOperationType().getCode();
229                                                if (resOpCode != null) {
230                                                        TypeRestfulInteractionEnum resOp = TypeRestfulInteractionEnum.VALUESET_BINDER.fromCodeString(resOpCode);
231                                                        if (resOp != null) {
232                                                                if (resourceOps.contains(resOp) == false) {
233                                                                        resourceOps.add(resOp);
234                                                                        resource.addInteraction().setCode(resOp);
235                                                                }
236                                                                if ("vread".equals(resOpCode)) {
237                                                                        // vread implies read
238                                                                        resOp = TypeRestfulInteractionEnum.READ;
239                                                                        if (resourceOps.contains(resOp) == false) {
240                                                                                resourceOps.add(resOp);
241                                                                                resource.addInteraction().setCode(resOp);
242                                                                        }
243                                                                }
244
245                                                                if (nextMethodBinding.isSupportsConditional()) {
246                                                                        switch (resOp) {
247                                                                                case CREATE:
248                                                                                        resource.setConditionalCreate(true);
249                                                                                        break;
250                                                                                case DELETE:
251                                                                                        if (nextMethodBinding.isSupportsConditionalMultiple()) {
252                                                                                                resource.setConditionalDelete(ConditionalDeleteStatusEnum.MULTIPLE_DELETES_SUPPORTED);
253                                                                                        } else {
254                                                                                                resource.setConditionalDelete(ConditionalDeleteStatusEnum.SINGLE_DELETES_SUPPORTED);
255                                                                                        }
256                                                                                        break;
257                                                                                case UPDATE:
258                                                                                        resource.setConditionalUpdate(true);
259                                                                                        break;
260                                                                                default:
261                                                                                        break;
262                                                                        }
263                                                                }
264                                                        }
265                                                }
266                                        }
267
268                                        checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
269
270                                        if (nextMethodBinding instanceof SearchMethodBinding) {
271                                                handleSearchMethodBinding(resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails);
272                                        } else if (nextMethodBinding instanceof OperationMethodBinding) {
273                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
274                                                String opName = bindings.getOperationBindingToId().get(methodBinding);
275                                                if (operationNames.add(opName)) {
276                                                        // Only add each operation (by name) once
277                                                        rest.addOperation().setName(methodBinding.getName().substring(1)).getDefinition().setReference("OperationDefinition/" + opName);
278                                                }
279                                        }
280
281                                        Collections.sort(resource.getInteraction(), new Comparator<RestResourceInteraction>() {
282                                                @Override
283                                                public int compare(RestResourceInteraction theO1, RestResourceInteraction theO2) {
284                                                        TypeRestfulInteractionEnum o1 = theO1.getCodeElement().getValueAsEnum();
285                                                        TypeRestfulInteractionEnum o2 = theO2.getCodeElement().getValueAsEnum();
286                                                        if (o1 == null && o2 == null) {
287                                                                return 0;
288                                                        }
289                                                        if (o1 == null) {
290                                                                return 1;
291                                                        }
292                                                        if (o2 == null) {
293                                                                return -1;
294                                                        }
295                                                        return o1.ordinal() - o2.ordinal();
296                                                }
297                                        });
298
299                                }
300
301                                for (String nextInclude : includes) {
302                                        resource.addSearchInclude(nextInclude);
303                                }
304                        } else {
305                                for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
306                                        checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
307                                        if (nextMethodBinding instanceof OperationMethodBinding) {
308                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
309                                                String opName = bindings.getOperationBindingToId().get(methodBinding);
310                                                if (operationNames.add(opName)) {
311                                                        rest.addOperation().setName(methodBinding.getName().substring(1)).getDefinition().setReference("OperationDefinition/" + opName);
312                                                }
313                                        }
314                                }
315                        }
316                }
317
318                return retVal;
319        }
320
321        private void handleSearchMethodBinding(RestResource resource, RuntimeResourceDefinition def, TreeSet<String> includes, SearchMethodBinding searchMethodBinding, RequestDetails theRequestDetails) {
322                includes.addAll(searchMethodBinding.getIncludes());
323
324                List<IParameter> params = searchMethodBinding.getParameters();
325                List<SearchParameter> searchParameters = new ArrayList<>();
326                for (IParameter nextParameter : params) {
327                        if ((nextParameter instanceof SearchParameter)) {
328                                searchParameters.add((SearchParameter) nextParameter);
329                        }
330                }
331                sortSearchParameters(searchParameters);
332                if (!searchParameters.isEmpty()) {
333                        // boolean allOptional = searchParameters.get(0).isRequired() == false;
334                        //
335                        // OperationDefinition query = null;
336                        // if (!allOptional) {
337                        // RestOperation operation = rest.addOperation();
338                        // query = new OperationDefinition();
339                        // operation.setDefinition(new ResourceReferenceDt(query));
340                        // query.getDescriptionElement().setValue(searchMethodBinding.getDescription());
341                        // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName));
342                        // for (String nextInclude : searchMethodBinding.getIncludes()) {
343                        // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude));
344                        // }
345                        // }
346
347                        for (SearchParameter nextParameter : searchParameters) {
348
349                                String nextParamName = nextParameter.getName();
350
351                                String chain = null;
352                                String nextParamUnchainedName = nextParamName;
353                                if (nextParamName.contains(".")) {
354                                        chain = nextParamName.substring(nextParamName.indexOf('.') + 1);
355                                        nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
356                                }
357
358                                String nextParamDescription = nextParameter.getDescription();
359
360                                /*
361                                 * If the parameter has no description, default to the one from the resource
362                                 */
363                                if (StringUtils.isBlank(nextParamDescription)) {
364                                        RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
365                                        if (paramDef != null) {
366                                                nextParamDescription = paramDef.getDescription();
367                                        }
368                                }
369
370                                String finalNextParamUnchainedName = nextParamUnchainedName;
371                                RestResourceSearchParam param =
372                                        resource
373                                                .getSearchParam()
374                                                .stream()
375                                                .filter(t -> t.getName().equals(finalNextParamUnchainedName))
376                                                .findFirst()
377                                                .orElseGet(() -> resource.addSearchParam());
378
379                                param.setName(nextParamUnchainedName);
380                                if (StringUtils.isNotBlank(chain)) {
381                                        param.addChain(chain);
382                                } else {
383                                        if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
384                                                for (String nextWhitelist : new TreeSet<>(nextParameter.getQualifierWhitelist())) {
385                                                        if (nextWhitelist.startsWith(".")) {
386                                                                param.addChain(nextWhitelist.substring(1));
387                                                        }
388                                                }
389                                        }
390                                }
391
392                                param.setDocumentation(nextParamDescription);
393                                if (nextParameter.getParamType() != null) {
394                                        param.getTypeElement().setValueAsString(nextParameter.getParamType().getCode());
395                                }
396                                for (Class<? extends IBaseResource> nextTarget : nextParameter.getDeclaredTypes()) {
397                                        RuntimeResourceDefinition targetDef = getServerConfiguration(theRequestDetails).getFhirContext().getResourceDefinition(nextTarget);
398                                        if (targetDef != null) {
399                                                ResourceTypeEnum code = ResourceTypeEnum.VALUESET_BINDER.fromCodeString(targetDef.getName());
400                                                if (code != null) {
401                                                        param.addTarget(code);
402                                                }
403                                        }
404                                }
405                        }
406                }
407        }
408
409
410        @Read(type = OperationDefinition.class)
411        public OperationDefinition readOperationDefinition(@IdParam IdDt theId, RequestDetails theRequestDetails) {
412                if (theId == null || theId.hasIdPart() == false) {
413                        throw new ResourceNotFoundException(theId);
414                }
415                RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails);
416                Bindings bindings = serverConfiguration.provideBindings();
417
418                List<OperationMethodBinding> sharedDescriptions = bindings.getOperationIdToBindings().get(theId.getIdPart());
419                if (sharedDescriptions == null || sharedDescriptions.isEmpty()) {
420                        throw new ResourceNotFoundException(theId);
421                }
422
423                OperationDefinition op = new OperationDefinition();
424                op.setStatus(ConformanceResourceStatusEnum.ACTIVE);
425                op.setKind(OperationKindEnum.OPERATION);
426                op.setIdempotent(true);
427
428                Set<String> inParams = new HashSet<>();
429                Set<String> outParams = new HashSet<>();
430
431                for (OperationMethodBinding sharedDescription : sharedDescriptions) {
432                        if (isNotBlank(sharedDescription.getDescription())) {
433                                op.setDescription(sharedDescription.getDescription());
434                        }
435                        if (!sharedDescription.isIdempotent()) {
436                                op.setIdempotent(sharedDescription.isIdempotent());
437                        }
438                        op.setCode(sharedDescription.getName().substring(1));
439                        if (sharedDescription.isCanOperateAtInstanceLevel()) {
440                                op.setInstance(sharedDescription.isCanOperateAtInstanceLevel());
441                        }
442                        if (sharedDescription.isCanOperateAtServerLevel()) {
443                                op.setSystem(sharedDescription.isCanOperateAtServerLevel());
444                        }
445                        if (isNotBlank(sharedDescription.getResourceName())) {
446                                op.addType().setValue(sharedDescription.getResourceName());
447                        }
448
449                        for (IParameter nextParamUntyped : sharedDescription.getParameters()) {
450                                if (nextParamUntyped instanceof OperationParameter) {
451                                        OperationParameter nextParam = (OperationParameter) nextParamUntyped;
452                                        if (!inParams.add(nextParam.getName())) {
453                                                continue;
454                                        }
455                                        Parameter param = op.addParameter();
456                                        param.setUse(OperationParameterUseEnum.IN);
457                                        if (nextParam.getParamType() != null) {
458                                                param.setType(nextParam.getParamType());
459                                        }
460                                        param.setMin(nextParam.getMin());
461                                        param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
462                                        param.setName(nextParam.getName());
463                                }
464                        }
465
466                        for (ReturnType nextParam : sharedDescription.getReturnParams()) {
467                                if (!outParams.add(nextParam.getName())) {
468                                        continue;
469                                }
470                                Parameter param = op.addParameter();
471                                param.setUse(OperationParameterUseEnum.OUT);
472                                if (nextParam.getType() != null) {
473                                        param.setType(nextParam.getType());
474                                }
475                                param.setMin(nextParam.getMin());
476                                param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
477                                param.setName(nextParam.getName());
478                        }
479                }
480
481                if (isBlank(op.getName())) {
482                        if (isNotBlank(op.getDescription())) {
483                                op.setName(op.getDescription());
484                        } else {
485                                op.setName(op.getCode());
486                        }
487                }
488
489                if (op.getSystem() == null) {
490                        op.setSystem(false);
491                }
492                if (op.getInstance() == null) {
493                        op.setInstance(false);
494                }
495
496                return op;
497        }
498
499        /**
500         * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation.
501         * <p>
502         * See the class documentation for an important note if you are extending this class
503         * </p>
504         * @deprecated Since 4.0.0 this does nothing
505         */
506        @Deprecated
507        public void setCache(boolean theCache) {
508                // nothing
509        }
510
511        @Override
512        public void setRestfulServer(RestfulServer theRestfulServer) {
513                // nothing
514        }
515
516        private void sortSearchParameters(List<SearchParameter> searchParameters) {
517                Collections.sort(searchParameters, new Comparator<SearchParameter>() {
518                        @Override
519                        public int compare(SearchParameter theO1, SearchParameter theO2) {
520                                if (theO1.isRequired() == theO2.isRequired()) {
521                                        return theO1.getName().compareTo(theO2.getName());
522                                }
523                                if (theO1.isRequired()) {
524                                        return -1;
525                                }
526                                return 1;
527                        }
528                });
529        }
530
531}