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