001package org.hl7.fhir.r4.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.server.RequestDetails;
012import ca.uhn.fhir.rest.server.Bindings;
013import ca.uhn.fhir.rest.server.IServerConformanceProvider;
014import ca.uhn.fhir.rest.server.RestfulServer;
015import ca.uhn.fhir.rest.server.RestfulServerConfiguration;
016import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
017import ca.uhn.fhir.rest.server.method.*;
018import ca.uhn.fhir.rest.server.method.SearchParameter;
019import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType;
020import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider;
021import org.apache.commons.lang3.StringUtils;
022import org.hl7.fhir.exceptions.FHIRException;
023import org.hl7.fhir.instance.model.api.IBaseResource;
024import org.hl7.fhir.instance.model.api.IPrimitiveType;
025import org.hl7.fhir.r4.model.*;
026import org.hl7.fhir.r4.model.CapabilityStatement.*;
027import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
028import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent;
029import org.hl7.fhir.r4.model.OperationDefinition.OperationKind;
030import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse;
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
040import ca.uhn.fhir.context.FhirContext;
041
042/*
043 * #%L
044 * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0)
045 * %%
046 * Copyright (C) 2014 - 2015 University Health Network
047 * %%
048 * Licensed under the Apache License, Version 2.0 (the "License");
049 * you may not use this file except in compliance with the License.
050 * You may obtain a copy of the License at
051 *
052 *      http://www.apache.org/licenses/LICENSE-2.0
053 *
054 * Unless required by applicable law or agreed to in writing, software
055 * distributed under the License is distributed on an "AS IS" BASIS,
056 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
057 * See the License for the specific language governing permissions and
058 * limitations under the License.
059 * #L%
060 */
061
062/**
063 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation
064 *
065 * <p>
066 * Note: This class is safe to extend, but it is important to note that the same instance of {@link CapabilityStatement} is always returned unless {@link #setCache(boolean)} is called with a value of
067 * <code>false</code>. This means that if you are adding anything to the returned conformance instance on each call you should call <code>setCache(false)</code> in your provider constructor.
068 * </p>
069 */
070public class ServerCapabilityStatementProvider extends BaseServerCapabilityStatementProvider implements IServerConformanceProvider<CapabilityStatement> {
071
072  private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProvider.class);
073  private String myPublisher = "Not provided";
074
075  /**
076   * No-arg constructor and setter so that the ServerConformanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen.
077   */
078  public ServerCapabilityStatementProvider() {
079    super();
080  }
081
082  /**
083   * Constructor
084   *
085   * @deprecated Use no-args constructor instead. Deprecated in 4.0.0
086   */
087  @Deprecated
088  public ServerCapabilityStatementProvider(RestfulServer theRestfulServer) {
089    this();
090  }
091
092  /**
093   * Constructor - This is intended only for JAX-RS server
094   */
095  public ServerCapabilityStatementProvider(RestfulServerConfiguration theServerConfiguration) {
096    super(theServerConfiguration);
097  }
098
099  private void checkBindingForSystemOps(CapabilityStatementRestComponent rest, Set<SystemRestfulInteraction> systemOps, BaseMethodBinding<?> nextMethodBinding) {
100    if (nextMethodBinding.getRestOperationType() != null) {
101      String sysOpCode = nextMethodBinding.getRestOperationType().getCode();
102      if (sysOpCode != null) {
103        SystemRestfulInteraction sysOp;
104        try {
105          sysOp = SystemRestfulInteraction.fromCode(sysOpCode);
106        } catch (FHIRException e) {
107          return;
108        }
109        if (sysOp == null) {
110          return;
111        }
112        if (systemOps.contains(sysOp) == false) {
113          systemOps.add(sysOp);
114          rest.addInteraction().setCode(sysOp);
115        }
116      }
117    }
118  }
119
120  private DateTimeType conformanceDate(RequestDetails theRequestDetails) {
121    IPrimitiveType<Date> buildDate = getServerConfiguration(theRequestDetails).getConformanceDate();
122    if (buildDate != null && buildDate.getValue() != null) {
123      try {
124        return new DateTimeType(buildDate.getValueAsString());
125      } catch (DataFormatException e) {
126        // fall through
127      }
128    }
129    return DateTimeType.now();
130  }
131
132
133  /**
134   * 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
135   * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
136   */
137  public String getPublisher() {
138    return myPublisher;
139  }
140
141  /**
142   * 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
143   * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
144   */
145  public void setPublisher(String thePublisher) {
146    myPublisher = thePublisher;
147  }
148
149  @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
150  @Override
151  @Metadata
152  public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
153
154    RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails);
155    Bindings bindings = configuration.provideBindings();
156
157    CapabilityStatement retVal = new CapabilityStatement();
158
159    retVal.setPublisher(myPublisher);
160    retVal.setDateElement(conformanceDate(theRequestDetails));
161    retVal.setFhirVersion(Enumerations.FHIRVersion.fromCode(FhirVersionEnum.R4.getFhirVersionString()));
162
163    ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE));
164    String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest);
165    retVal
166      .getImplementation()
167      .setUrl(serverBase)
168      .setDescription(configuration.getImplementationDescription());
169
170    retVal.setKind(CapabilityStatementKind.INSTANCE);
171    retVal.getSoftware().setName(configuration.getServerName());
172    retVal.getSoftware().setVersion(configuration.getServerVersion());
173    retVal.addFormat(Constants.CT_FHIR_XML_NEW);
174    retVal.addFormat(Constants.CT_FHIR_JSON_NEW);
175    retVal.setStatus(PublicationStatus.ACTIVE);
176
177    CapabilityStatementRestComponent rest = retVal.addRest();
178    rest.setMode(RestfulCapabilityMode.SERVER);
179
180    Set<SystemRestfulInteraction> systemOps = new HashSet<>();
181    Set<String> operationNames = new HashSet<>();
182
183    Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings();
184    Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype();
185    for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
186
187      if (nextEntry.getKey().isEmpty() == false) {
188        Set<TypeRestfulInteraction> resourceOps = new HashSet<>();
189        CapabilityStatementRestResourceComponent resource = rest.addResource();
190        String resourceName = nextEntry.getKey();
191        
192        RuntimeResourceDefinition def;
193        FhirContext context = configuration.getFhirContext();
194        if (resourceNameToSharedSupertype.containsKey(resourceName)) {
195          def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName));
196        } else {
197          def = context.getResourceDefinition(resourceName);
198        }
199        resource.getTypeElement().setValue(def.getName());
200        resource.getProfileElement().setValue((def.getResourceProfile(serverBase)));
201
202        TreeSet<String> includes = new TreeSet<>();
203
204        // Map<String, CapabilityStatement.RestResourceSearchParam> nameToSearchParam = new HashMap<String,
205        // CapabilityStatement.RestResourceSearchParam>();
206        for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
207          nextMethodBinding.getRestOperationType();
208          String resOpCode = nextMethodBinding.getRestOperationType().getCode();
209          if (resOpCode != null) {
210            TypeRestfulInteraction resOp;
211            try {
212              resOp = TypeRestfulInteraction.fromCode(resOpCode);
213            } catch (Exception e) {
214              resOp = null;
215            }
216            if (resOp != null) {
217              if (resourceOps.contains(resOp) == false) {
218                resourceOps.add(resOp);
219                resource.addInteraction().setCode(resOp);
220              }
221              if ("vread".equals(resOpCode)) {
222                // vread implies read
223                resOp = TypeRestfulInteraction.READ;
224                if (resourceOps.contains(resOp) == false) {
225                  resourceOps.add(resOp);
226                  resource.addInteraction().setCode(resOp);
227                }
228              }
229
230              if (nextMethodBinding.isSupportsConditional()) {
231                switch (resOp) {
232                  case CREATE:
233                    resource.setConditionalCreate(true);
234                    break;
235                  case DELETE:
236                    if (nextMethodBinding.isSupportsConditionalMultiple()) {
237                      resource.setConditionalDelete(ConditionalDeleteStatus.MULTIPLE);
238                    } else {
239                      resource.setConditionalDelete(ConditionalDeleteStatus.SINGLE);
240                    }
241                    break;
242                  case UPDATE:
243                    resource.setConditionalUpdate(true);
244                    break;
245                  default:
246                    break;
247                }
248              }
249            }
250          }
251
252          checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
253
254          if (nextMethodBinding instanceof SearchMethodBinding) {
255            SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding;
256            if (methodBinding.getQueryName() != null) {
257              String queryName = bindings.getNamedSearchMethodBindingToName().get(methodBinding);
258              if (operationNames.add(queryName)) {
259                rest.addOperation().setName(methodBinding.getQueryName()).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName));
260              }
261            } else {
262              handleNamelessSearchMethodBinding(resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails);
263            }
264          } else if (nextMethodBinding instanceof OperationMethodBinding) {
265            OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
266            String opName = bindings.getOperationBindingToName().get(methodBinding);
267            if (operationNames.add(opName)) {
268              // Only add each operation (by name) once
269              rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName));
270            }
271          }
272
273          resource.getInteraction().sort(new Comparator<ResourceInteractionComponent>() {
274            @Override
275            public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) {
276              TypeRestfulInteraction o1 = theO1.getCode();
277              TypeRestfulInteraction o2 = theO2.getCode();
278              if (o1 == null && o2 == null) {
279                return 0;
280              }
281              if (o1 == null) {
282                return 1;
283              }
284              if (o2 == null) {
285                return -1;
286              }
287              return o1.ordinal() - o2.ordinal();
288            }
289          });
290
291        }
292
293        for (String nextInclude : includes) {
294          resource.addSearchInclude(nextInclude);
295        }
296      } else {
297        for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
298          checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
299          if (nextMethodBinding instanceof OperationMethodBinding) {
300            OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
301            String opName = bindings.getOperationBindingToName().get(methodBinding);
302            if (operationNames.add(opName)) {
303              ourLog.debug("Found bound operation: {}", opName);
304              rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition((getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName));
305            }
306          }
307        }
308      }
309    }
310
311    return retVal;
312  }
313
314  protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) {
315    if (theRequestDetails == null) {
316      return "";
317    }
318    return theRequestDetails.getServerBaseForRequest() + "/";
319  }
320
321  private void handleNamelessSearchMethodBinding(CapabilityStatementRestResourceComponent resource, RuntimeResourceDefinition def, TreeSet<String> includes,
322                                                 SearchMethodBinding searchMethodBinding, RequestDetails theRequestDetails) {
323    includes.addAll(searchMethodBinding.getIncludes());
324
325    List<IParameter> params = searchMethodBinding.getParameters();
326    List<SearchParameter> searchParameters = new ArrayList<>();
327    for (IParameter nextParameter : params) {
328      if ((nextParameter instanceof SearchParameter)) {
329        searchParameters.add((SearchParameter) nextParameter);
330      }
331    }
332    sortSearchParameters(searchParameters);
333    if (!searchParameters.isEmpty()) {
334
335      Set<String> paramNames = new HashSet<>();
336      for (SearchParameter nextParameter : searchParameters) {
337
338        if (nextParameter.getParamType() == null) {
339          ourLog.warn("SearchParameter {}:{} does not declare a type - Not exporting in CapabilityStatement", def.getName(), nextParameter.getName());
340          continue;
341        }
342
343        String nextParamName = nextParameter.getName();
344
345        String nextParamUnchainedName = nextParamName;
346        if (nextParamName.contains(".")) {
347          nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
348        }
349
350        if (!paramNames.add(nextParamUnchainedName)) {
351          continue;
352        }
353
354        String nextParamDescription = nextParameter.getDescription();
355
356        /*
357         * If the parameter has no description, default to the one from the resource
358         */
359        if (StringUtils.isBlank(nextParamDescription)) {
360          RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
361          if (paramDef != null) {
362            nextParamDescription = paramDef.getDescription();
363          }
364        }
365
366
367        CapabilityStatementRestResourceSearchParamComponent param = resource.addSearchParam();
368        String typeCode = nextParameter.getParamType().getCode();
369        param.getTypeElement().setValueAsString(typeCode);
370        param.setName(nextParamUnchainedName);
371        param.setDocumentation(nextParamDescription);
372
373      }
374    }
375  }
376
377
378  @Read(type = OperationDefinition.class)
379  public OperationDefinition readOperationDefinition(@IdParam IdType theId, RequestDetails theRequestDetails) {
380    if (theId == null || theId.hasIdPart() == false) {
381      throw new ResourceNotFoundException(theId);
382    }
383    RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails);
384    Bindings bindings = configuration.provideBindings();
385
386    List<OperationMethodBinding> operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart());
387    if (operationBindings != null && !operationBindings.isEmpty()) {
388      return readOperationDefinitionForOperation(operationBindings);
389    }
390    List<SearchMethodBinding> searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart());
391    if (searchBindings != null && !searchBindings.isEmpty()) {
392      return readOperationDefinitionForNamedSearch(searchBindings);
393    }
394    throw new ResourceNotFoundException(theId);
395  }
396
397  private OperationDefinition readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) {
398    OperationDefinition op = new OperationDefinition();
399    op.setStatus(PublicationStatus.ACTIVE);
400    op.setKind(OperationKind.QUERY);
401    op.setAffectsState(false);
402
403    op.setSystem(false);
404    op.setType(false);
405    op.setInstance(false);
406
407    Set<String> inParams = new HashSet<>();
408
409    for (SearchMethodBinding binding : bindings) {
410      if (isNotBlank(binding.getDescription())) {
411        op.setDescription(binding.getDescription());
412      }
413      if (isBlank(binding.getResourceProviderResourceName())) {
414        op.setSystem(true);
415      } else {
416        op.setType(true);
417        op.addResourceElement().setValue(binding.getResourceProviderResourceName());
418      }
419      op.setCode(binding.getQueryName());
420      for (IParameter nextParamUntyped : binding.getParameters()) {
421        if (nextParamUntyped instanceof SearchParameter) {
422          SearchParameter nextParam = (SearchParameter) nextParamUntyped;
423          if (!inParams.add(nextParam.getName())) {
424            continue;
425          }
426          OperationDefinitionParameterComponent param = op.addParameter();
427          param.setUse(OperationParameterUse.IN);
428          param.setType("string");
429          param.getSearchTypeElement().setValueAsString(nextParam.getParamType().getCode());
430          param.setMin(nextParam.isRequired() ? 1 : 0);
431          param.setMax("1");
432          param.setName(nextParam.getName());
433
434
435        }
436      }
437
438      if (isBlank(op.getName())) {
439        if (isNotBlank(op.getDescription())) {
440          op.setName(op.getDescription());
441        } else {
442          op.setName(op.getCode());
443        }
444      }
445    }
446
447    return op;
448  }
449
450  private OperationDefinition readOperationDefinitionForOperation(List<OperationMethodBinding> bindings) {
451    OperationDefinition op = new OperationDefinition();
452    op.setStatus(PublicationStatus.ACTIVE);
453    op.setKind(OperationKind.OPERATION);
454    op.setAffectsState(false);
455
456    // We reset these to true below if we find a binding that can handle the level
457    op.setSystem(false);
458    op.setType(false);
459    op.setInstance(false);
460
461    Set<String> inParams = new HashSet<>();
462    Set<String> outParams = new HashSet<>();
463
464    for (OperationMethodBinding sharedDescription : bindings) {
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.isCanOperateAtTypeLevel()) {
475        op.setType(true);
476      }
477      if (!sharedDescription.isIdempotent()) {
478        op.setAffectsState(!sharedDescription.isIdempotent());
479      }
480      op.setCode(sharedDescription.getName().substring(1));
481      if (sharedDescription.isCanOperateAtInstanceLevel()) {
482        op.setInstance(sharedDescription.isCanOperateAtInstanceLevel());
483      }
484      if (sharedDescription.isCanOperateAtServerLevel()) {
485        op.setSystem(sharedDescription.isCanOperateAtServerLevel());
486      }
487      if (isNotBlank(sharedDescription.getResourceName())) {
488        op.addResourceElement().setValue(sharedDescription.getResourceName());
489      }
490
491      for (IParameter nextParamUntyped : sharedDescription.getParameters()) {
492        if (nextParamUntyped instanceof OperationParameter) {
493          OperationParameter nextParam = (OperationParameter) nextParamUntyped;
494          OperationDefinitionParameterComponent param = op.addParameter();
495          if (!inParams.add(nextParam.getName())) {
496            continue;
497          }
498          param.setUse(OperationParameterUse.IN);
499          if (nextParam.getParamType() != null) {
500            param.setType(nextParam.getParamType());
501          }
502          if (nextParam.getSearchParamType() != null) {
503            param.getSearchTypeElement().setValueAsString(nextParam.getSearchParamType());
504          }
505          param.setMin(nextParam.getMin());
506          param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
507          param.setName(nextParam.getName());
508        }
509      }
510
511      for (ReturnType nextParam : sharedDescription.getReturnParams()) {
512        if (!outParams.add(nextParam.getName())) {
513          continue;
514        }
515        OperationDefinitionParameterComponent param = op.addParameter();
516        param.setUse(OperationParameterUse.OUT);
517        if (nextParam.getType() != null) {
518          param.setType(nextParam.getType());
519        }
520        param.setMin(nextParam.getMin());
521        param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
522        param.setName(nextParam.getName());
523      }
524    }
525
526    if (isBlank(op.getName())) {
527      if (isNotBlank(op.getDescription())) {
528        op.setName(op.getDescription());
529      } else {
530        op.setName(op.getCode());
531      }
532    }
533
534    if (op.hasSystem() == false) {
535      op.setSystem(false);
536    }
537    if (op.hasInstance() == false) {
538      op.setInstance(false);
539    }
540
541    return op;
542  }
543
544  /**
545   * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation.
546   * <p>
547   * See the class documentation for an important note if you are extending this class
548   * </p>
549   *
550   * @deprecated Since 4.0.0 - This method no longer does anything
551   */
552  @Deprecated
553  public ServerCapabilityStatementProvider setCache(boolean theCache) {
554    return this;
555  }
556
557  @Override
558  public void setRestfulServer(RestfulServer theRestfulServer) {
559    // ignore
560  }
561
562  private void sortSearchParameters(List<SearchParameter> searchParameters) {
563    Collections.sort(searchParameters, new Comparator<SearchParameter>() {
564      @Override
565      public int compare(SearchParameter theO1, SearchParameter theO2) {
566        if (theO1.isRequired() == theO2.isRequired()) {
567          return theO1.getName().compareTo(theO2.getName());
568        }
569        if (theO1.isRequired()) {
570          return -1;
571        }
572        return 1;
573      }
574    });
575  }
576}