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