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(("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(("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(("OperationDefinition/" + opName));
305            }
306          }
307        }
308      }
309    }
310
311    return retVal;
312  }
313
314  private void handleNamelessSearchMethodBinding(CapabilityStatementRestResourceComponent resource, RuntimeResourceDefinition def, TreeSet<String> includes,
315                                                 SearchMethodBinding searchMethodBinding, RequestDetails theRequestDetails) {
316    includes.addAll(searchMethodBinding.getIncludes());
317
318    List<IParameter> params = searchMethodBinding.getParameters();
319    List<SearchParameter> searchParameters = new ArrayList<>();
320    for (IParameter nextParameter : params) {
321      if ((nextParameter instanceof SearchParameter)) {
322        searchParameters.add((SearchParameter) nextParameter);
323      }
324    }
325    sortSearchParameters(searchParameters);
326    if (!searchParameters.isEmpty()) {
327
328      for (SearchParameter nextParameter : searchParameters) {
329
330        if (nextParameter.getParamType() == null) {
331          ourLog.warn("SearchParameter {}:{} does not declare a type - Not exporting in CapabilityStatement", def.getName(), nextParameter.getName());
332          continue;
333        }
334
335        String nextParamName = nextParameter.getName();
336
337        String nextParamUnchainedName = nextParamName;
338        if (nextParamName.contains(".")) {
339          nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
340        }
341
342        String nextParamDescription = nextParameter.getDescription();
343
344        /*
345         * If the parameter has no description, default to the one from the resource
346         */
347        if (StringUtils.isBlank(nextParamDescription)) {
348          RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
349          if (paramDef != null) {
350            nextParamDescription = paramDef.getDescription();
351          }
352        }
353
354
355        CapabilityStatementRestResourceSearchParamComponent param = resource.addSearchParam();
356        String typeCode = nextParameter.getParamType().getCode();
357        param.getTypeElement().setValueAsString(typeCode);
358        param.setName(nextParamUnchainedName);
359        param.setDocumentation(nextParamDescription);
360
361      }
362    }
363  }
364
365
366  @Read(type = OperationDefinition.class)
367  public OperationDefinition readOperationDefinition(@IdParam IdType theId, RequestDetails theRequestDetails) {
368    if (theId == null || theId.hasIdPart() == false) {
369      throw new ResourceNotFoundException(theId);
370    }
371    RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails);
372    Bindings bindings = configuration.provideBindings();
373
374    List<OperationMethodBinding> operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart());
375    if (operationBindings != null && !operationBindings.isEmpty()) {
376      return readOperationDefinitionForOperation(operationBindings);
377    }
378    List<SearchMethodBinding> searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart());
379    if (searchBindings != null && !searchBindings.isEmpty()) {
380      return readOperationDefinitionForNamedSearch(searchBindings);
381    }
382    throw new ResourceNotFoundException(theId);
383  }
384
385  private OperationDefinition readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) {
386    OperationDefinition op = new OperationDefinition();
387    op.setStatus(PublicationStatus.ACTIVE);
388    op.setKind(OperationKind.QUERY);
389    op.setAffectsState(false);
390
391    op.setSystem(false);
392    op.setType(false);
393    op.setInstance(false);
394
395    Set<String> inParams = new HashSet<>();
396
397    for (SearchMethodBinding binding : bindings) {
398      if (isNotBlank(binding.getDescription())) {
399        op.setDescription(binding.getDescription());
400      }
401      if (isBlank(binding.getResourceProviderResourceName())) {
402        op.setSystem(true);
403      } else {
404        op.setType(true);
405        op.addResourceElement().setValue(binding.getResourceProviderResourceName());
406      }
407      op.setCode(binding.getQueryName());
408      for (IParameter nextParamUntyped : binding.getParameters()) {
409        if (nextParamUntyped instanceof SearchParameter) {
410          SearchParameter nextParam = (SearchParameter) nextParamUntyped;
411          if (!inParams.add(nextParam.getName())) {
412            continue;
413          }
414          OperationDefinitionParameterComponent param = op.addParameter();
415          param.setUse(OperationParameterUse.IN);
416          param.setType("string");
417          param.getSearchTypeElement().setValueAsString(nextParam.getParamType().getCode());
418          param.setMin(nextParam.isRequired() ? 1 : 0);
419          param.setMax("1");
420          param.setName(nextParam.getName());
421        }
422      }
423
424      if (isBlank(op.getName())) {
425        if (isNotBlank(op.getDescription())) {
426          op.setName(op.getDescription());
427        } else {
428          op.setName(op.getCode());
429        }
430      }
431    }
432
433    return op;
434  }
435
436  private OperationDefinition readOperationDefinitionForOperation(List<OperationMethodBinding> bindings) {
437    OperationDefinition op = new OperationDefinition();
438    op.setStatus(PublicationStatus.ACTIVE);
439    op.setKind(OperationKind.OPERATION);
440    op.setAffectsState(false);
441
442    // We reset these to true below if we find a binding that can handle the level
443    op.setSystem(false);
444    op.setType(false);
445    op.setInstance(false);
446
447    Set<String> inParams = new HashSet<>();
448    Set<String> outParams = new HashSet<>();
449
450    for (OperationMethodBinding sharedDescription : bindings) {
451      if (isNotBlank(sharedDescription.getDescription())) {
452        op.setDescription(sharedDescription.getDescription());
453      }
454      if (sharedDescription.isCanOperateAtInstanceLevel()) {
455        op.setInstance(true);
456      }
457      if (sharedDescription.isCanOperateAtServerLevel()) {
458        op.setSystem(true);
459      }
460      if (sharedDescription.isCanOperateAtTypeLevel()) {
461        op.setType(true);
462      }
463      if (!sharedDescription.isIdempotent()) {
464        op.setAffectsState(!sharedDescription.isIdempotent());
465      }
466      op.setCode(sharedDescription.getName().substring(1));
467      if (sharedDescription.isCanOperateAtInstanceLevel()) {
468        op.setInstance(sharedDescription.isCanOperateAtInstanceLevel());
469      }
470      if (sharedDescription.isCanOperateAtServerLevel()) {
471        op.setSystem(sharedDescription.isCanOperateAtServerLevel());
472      }
473      if (isNotBlank(sharedDescription.getResourceName())) {
474        op.addResourceElement().setValue(sharedDescription.getResourceName());
475      }
476
477      for (IParameter nextParamUntyped : sharedDescription.getParameters()) {
478        if (nextParamUntyped instanceof OperationParameter) {
479          OperationParameter nextParam = (OperationParameter) nextParamUntyped;
480          OperationDefinitionParameterComponent param = op.addParameter();
481          if (!inParams.add(nextParam.getName())) {
482            continue;
483          }
484          param.setUse(OperationParameterUse.IN);
485          if (nextParam.getParamType() != null) {
486            param.setType(nextParam.getParamType());
487          }
488          if (nextParam.getSearchParamType() != null) {
489            param.getSearchTypeElement().setValueAsString(nextParam.getSearchParamType());
490          }
491          param.setMin(nextParam.getMin());
492          param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
493          param.setName(nextParam.getName());
494        }
495      }
496
497      for (ReturnType nextParam : sharedDescription.getReturnParams()) {
498        if (!outParams.add(nextParam.getName())) {
499          continue;
500        }
501        OperationDefinitionParameterComponent param = op.addParameter();
502        param.setUse(OperationParameterUse.OUT);
503        if (nextParam.getType() != null) {
504          param.setType(nextParam.getType());
505        }
506        param.setMin(nextParam.getMin());
507        param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
508        param.setName(nextParam.getName());
509      }
510    }
511
512    if (isBlank(op.getName())) {
513      if (isNotBlank(op.getDescription())) {
514        op.setName(op.getDescription());
515      } else {
516        op.setName(op.getCode());
517      }
518    }
519
520    if (op.hasSystem() == false) {
521      op.setSystem(false);
522    }
523    if (op.hasInstance() == false) {
524      op.setInstance(false);
525    }
526
527    return op;
528  }
529
530  /**
531   * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation.
532   * <p>
533   * See the class documentation for an important note if you are extending this class
534   * </p>
535   *
536   * @deprecated Since 4.0.0 - This method no longer does anything
537   */
538  @Deprecated
539  public ServerCapabilityStatementProvider setCache(boolean theCache) {
540    return this;
541  }
542
543  @Override
544  public void setRestfulServer(RestfulServer theRestfulServer) {
545    // ignore
546  }
547
548  private void sortSearchParameters(List<SearchParameter> searchParameters) {
549    Collections.sort(searchParameters, new Comparator<SearchParameter>() {
550      @Override
551      public int compare(SearchParameter theO1, SearchParameter theO2) {
552        if (theO1.isRequired() == theO2.isRequired()) {
553          return theO1.getName().compareTo(theO2.getName());
554        }
555        if (theO1.isRequired()) {
556          return -1;
557        }
558        return 1;
559      }
560    });
561  }
562}