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