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