001package org.hl7.fhir.r5.terminologies.utilities;
002
003import java.util.ArrayList;
004import java.util.Collection;
005import java.util.List;
006
007import org.hl7.fhir.exceptions.FHIRException;
008import org.hl7.fhir.exceptions.TerminologyServiceException;
009import org.hl7.fhir.r5.context.BaseWorkerContext;
010import org.hl7.fhir.r5.context.ContextUtilities;
011import org.hl7.fhir.r5.context.IWorkerContext;
012import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
013import org.hl7.fhir.r5.extensions.ExtensionUtilities;
014import org.hl7.fhir.r5.model.BooleanType;
015import org.hl7.fhir.r5.model.CanonicalResource;
016import org.hl7.fhir.r5.model.CodeSystem;
017import org.hl7.fhir.r5.model.DataType;
018import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
019import org.hl7.fhir.r5.model.OperationOutcome.IssueType;
020import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent;
021import org.hl7.fhir.r5.model.Extension;
022import org.hl7.fhir.r5.model.Parameters;
023import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
024import org.hl7.fhir.r5.model.StringType;
025import org.hl7.fhir.r5.model.UriType;
026import org.hl7.fhir.r5.model.UrlType;
027import org.hl7.fhir.r5.model.ValueSet;
028import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent;
029
030import org.hl7.fhir.r5.terminologies.expansion.OperationIsTooCostly;
031import org.hl7.fhir.r5.terminologies.validation.VSCheckerException;
032import org.hl7.fhir.r5.utils.UserDataNames;
033import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
034import org.hl7.fhir.utilities.StandardsStatus;
035import org.hl7.fhir.utilities.Utilities;
036import org.hl7.fhir.utilities.VersionUtilities;
037import org.hl7.fhir.utilities.i18n.I18nConstants;
038import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
039
040import javax.annotation.Nonnull;
041
042@MarkedToMoveToAdjunctPackage
043public class ValueSetProcessBase {
044
045  public class UnknownValueSetException extends FHIRException {
046
047    protected UnknownValueSetException() {
048      super();
049    }
050
051    protected UnknownValueSetException(String message, Throwable cause) {
052      super(message, cause);
053    }
054
055    protected UnknownValueSetException(String message) {
056      super(message);
057    }
058
059    protected UnknownValueSetException(Throwable cause) {
060      super(cause);
061    }
062  }
063
064  public static class TerminologyOperationDetails {
065
066    private List<String> supplements;
067
068    public TerminologyOperationDetails(List<String> supplements) {
069      super();
070      this.supplements = supplements;
071    }
072
073    public void seeSupplement(CodeSystem supp) {
074      supplements.remove(supp.getUrl());
075      supplements.remove(supp.getVersionedUrl());
076    }
077  }
078  
079  public enum OpIssueCode {
080    NotInVS, ThisNotInVS, InvalidCode, Display, DisplayComment, NotFound, CodeRule, VSProcessing, InferFailed, StatusCheck, InvalidData, CodeComment, VersionError;
081    
082    public String toCode() {
083      switch (this) {
084      case CodeRule: return "code-rule";
085      case Display: return "invalid-display";
086      case DisplayComment: return "display-comment";
087      case InferFailed: return "cannot-infer";
088      case InvalidCode: return "invalid-code";
089      case NotFound: return "not-found";
090        case NotInVS: return "not-in-vs";
091      case InvalidData: return "invalid-data";
092      case StatusCheck: return "status-check";
093      case ThisNotInVS: return "this-code-not-in-vs";
094      case VSProcessing: return "vs-invalid";
095      case CodeComment: return "code-comment";
096      case VersionError: return "version-error";
097      default:
098        return "??";      
099      }
100    }
101  }
102  protected BaseWorkerContext context;
103  private ContextUtilities cu;
104  protected TerminologyOperationContext opContext;
105  protected List<String> requiredSupplements = new ArrayList<>();
106  protected List<String> allErrors = new ArrayList<>();
107
108  protected ValueSetProcessBase(BaseWorkerContext context, TerminologyOperationContext opContext) {
109    super();
110    this.context = context;
111    this.opContext = opContext;
112  }
113  public static class AlternateCodesProcessingRules {
114    private boolean all;
115    private List<String> uses = new ArrayList<>();
116    
117    public AlternateCodesProcessingRules(boolean b) {
118      all = b;
119    }
120
121    private void seeParameter(DataType value) {
122      if (value != null) {
123        if (value instanceof BooleanType) {
124          all = ((BooleanType) value).booleanValue();
125          uses.clear();
126        } else if (value.isPrimitive()) {
127          String s = value.primitiveValue();
128          if (!Utilities.noString(s)) {
129            uses.add(s);
130          }
131        }
132      }
133    }
134
135    public void seeParameters(Parameters pp) {
136      for (ParametersParameterComponent p : pp.getParameter()) {
137        String name = p.getName();
138        if ("includeAlternateCodes".equals(name)) {
139          DataType value = p.getValue();
140          seeParameter(value);
141        }
142      }
143    }
144
145    public void seeValueSet(ValueSet vs) {
146      if (vs != null) {
147        for (Extension ext : vs.getCompose().getExtension()) {
148          if (Utilities.existsInList(ext.getUrl(), ExtensionDefinitions.EXT_VS_EXP_PARAM_NEW, ExtensionDefinitions.EXT_VS_EXP_PARAM_OLD)) {
149            String name = ext.getExtensionString("name");
150            Extension value = ext.getExtensionByUrl("value");
151            if ("includeAlternateCodes".equals(name) && value != null && value.hasValue()) {
152              seeParameter(value.getValue());
153            }
154          }
155        }
156      }
157    }
158
159    public boolean passes(List<Extension> extensions) {
160      if (all) {
161        return true;
162      }
163
164      for (Extension ext : extensions) {
165        if (ExtensionDefinitions.EXT_CS_ALTERNATE_USE.equals(ext.getUrl())) {
166          if (ext.hasValueCoding() && Utilities.existsInList(ext.getValueCoding().getCode(), uses)) {
167            return true;
168          }
169        }
170      }
171      return false;
172    }
173  }
174
175  protected List<OperationOutcomeIssueComponent> makeIssue(IssueSeverity level, IssueType type, String location, String message, OpIssueCode code, String server, String msgId) {
176    OperationOutcomeIssueComponent result = new OperationOutcomeIssueComponent();
177    switch (level) {
178    case ERROR:
179      result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR);
180      break;
181    case FATAL:
182      result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.FATAL);
183      break;
184    case INFORMATION:
185      result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.INFORMATION);
186      break;
187    case WARNING:
188      result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.WARNING);
189      break;
190    }
191    result.setCode(type);
192    if (location != null) {
193      result.addLocation(location);
194      result.addExpression(location);
195    }
196    result.getDetails().setText(message);
197    if (code != null) {
198      result.getDetails().addCoding("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", code.toCode(), null);
199    }
200    if (server != null) {
201      result.addExtension(ExtensionDefinitions.EXT_ISSUE_SERVER, new UrlType(server));
202    }
203    if (msgId != null) {      
204      result.addExtension(ExtensionDefinitions.EXT_ISSUE_MSG_ID, new StringType(msgId));
205    }
206    ArrayList<OperationOutcomeIssueComponent> list = new ArrayList<>();
207    list.add(result);
208    return list;
209  }
210  
211  public void checkCanonical(List<OperationOutcomeIssueComponent> issues, String path, CanonicalResource resource, CanonicalResource source) {
212    if (resource != null) {
213      StandardsStatus standardsStatus = ExtensionUtilities.getStandardsStatus(resource);
214      if (standardsStatus == StandardsStatus.DEPRECATED) {
215        addToIssues(issues, makeStatusIssue(path, "deprecated", I18nConstants.MSG_DEPRECATED, resource));
216      } else if (standardsStatus == StandardsStatus.WITHDRAWN) {
217        addToIssues(issues, makeStatusIssue(path, "withdrawn", I18nConstants.MSG_WITHDRAWN, resource));
218      } else if (resource.getStatus() == PublicationStatus.RETIRED) {
219        addToIssues(issues, makeStatusIssue(path, "retired", I18nConstants.MSG_RETIRED, resource));
220      } else if (source != null) {
221        if (resource.getExperimental() && !source.getExperimental()) {
222          addToIssues(issues, makeStatusIssue(path, "experimental", I18nConstants.MSG_EXPERIMENTAL, resource));
223        } else if ((resource.getStatus() == PublicationStatus.DRAFT || standardsStatus == StandardsStatus.DRAFT)
224            && !(source.getStatus() == PublicationStatus.DRAFT || ExtensionUtilities.getStandardsStatus(source) == StandardsStatus.DRAFT)) {
225          addToIssues(issues, makeStatusIssue(path, "draft", I18nConstants.MSG_DRAFT, resource));
226        }
227      } else {
228        if (resource.getExperimental()) {
229          addToIssues(issues, makeStatusIssue(path, "experimental", I18nConstants.MSG_EXPERIMENTAL, resource));
230        } else if ((resource.getStatus() == PublicationStatus.DRAFT || standardsStatus == StandardsStatus.DRAFT)) {
231          addToIssues(issues, makeStatusIssue(path, "draft", I18nConstants.MSG_DRAFT, resource));
232        }
233      }
234    }
235  }
236
237  private List<OperationOutcomeIssueComponent> makeStatusIssue(String path, String id, String msg, CanonicalResource resource) {
238    List<OperationOutcomeIssueComponent> iss = makeIssue(IssueSeverity.INFORMATION, IssueType.BUSINESSRULE, null,
239      context.formatMessage(msg, resource.getVersionedUrl(), null, resource.fhirType()), OpIssueCode.StatusCheck, null, msg);
240
241    // this is a testing hack - see TerminologyServiceTests
242    iss.get(0).setUserData(UserDataNames.tx_status_msg_name, "warning-"+id);
243    iss.get(0).setUserData(UserDataNames.tx_status_msg_value, new UriType(resource.getVersionedUrl()));
244    ExtensionUtilities.setStringExtension(iss.get(0), ExtensionDefinitions.EXT_ISSUE_MSG_ID, msg);
245    
246    return iss;
247  }
248  
249  private void addToIssues(List<OperationOutcomeIssueComponent> issues, List<OperationOutcomeIssueComponent> toAdd) {
250    for (OperationOutcomeIssueComponent t : toAdd) {
251      boolean found = false;
252      for (OperationOutcomeIssueComponent i : issues) {
253        if (i.getSeverity() == t.getSeverity() && i.getCode() == t.getCode() && i.getDetails().getText().equals(t.getDetails().getText())) { // ignore location
254          found = true;
255        }
256      }
257      if (!found) {
258        issues.add(t);
259      }
260    }    
261  }
262
263  public void checkCanonical(ValueSetExpansionComponent params, CanonicalResource resource, ValueSet source) {
264    if (resource != null) {
265      StandardsStatus standardsStatus = ExtensionUtilities.getStandardsStatus(resource);
266      if (standardsStatus == StandardsStatus.DEPRECATED) {
267        if (!params.hasParameterValue("warning-deprecated", resource.getVersionedUrl())) {
268          params.addParameter("warning-deprecated", new UriType(resource.getVersionedUrl()));
269        } 
270      } else if (standardsStatus == StandardsStatus.WITHDRAWN) {
271        if (!params.hasParameterValue("warning-withdrawn", resource.getVersionedUrl())) {
272          params.addParameter("warning-withdrawn", new UriType(resource.getVersionedUrl()));
273        } 
274      } else if (resource.getStatus() == PublicationStatus.RETIRED) {
275        if (!params.hasParameterValue("warning-retired", resource.getVersionedUrl())) {
276          params.addParameter("warning-retired", new UriType(resource.getVersionedUrl()));
277        } 
278      } else if (resource.getExperimental() && !source.getExperimental()) {
279        if (!params.hasParameterValue("warning-experimental", resource.getVersionedUrl())) {
280          params.addParameter("warning-experimental", new UriType(resource.getVersionedUrl()));
281        }         
282      } else if ((resource.getStatus() == PublicationStatus.DRAFT || standardsStatus == StandardsStatus.DRAFT)
283          && !(source.getStatus() == PublicationStatus.DRAFT || ExtensionUtilities.getStandardsStatus(source) == StandardsStatus.DRAFT)) {
284        if (!params.hasParameterValue("warning-draft", resource.getVersionedUrl())) {
285          params.addParameter("warning-draft", new UriType(resource.getVersionedUrl()));
286        }         
287      }
288    }
289  }
290
291  public TerminologyOperationContext getOpContext() {
292    return opContext;
293  }
294
295                         
296  public ContextUtilities getCu() {
297    if (cu == null) {
298      cu = new ContextUtilities(context);
299    }
300    return cu;
301  }
302
303
304  public String removeSupplement(String s) {
305    requiredSupplements.remove(s);
306    if (s.contains("|")) {
307      s = s.substring(0, s.indexOf("|"));
308      requiredSupplements.remove(s);
309    }
310    return s;
311  }
312
313  protected boolean versionsMatch(@Nonnull String system, @Nonnull String candidate, @Nonnull String criteria) {
314    if (system == null || candidate == null || criteria == null) {
315      return false;
316    }
317    CodeSystem cs = context.fetchCodeSystem(system);
318    VersionAlgorithm va = cs == null ? VersionAlgorithm.Unknown : VersionAlgorithm.fromType(cs.getVersionAlgorithm());
319    if (va == VersionAlgorithm.Unknown) {
320      va = VersionAlgorithm.guessFormat(candidate);
321    }
322    switch (va) {
323      case Unknown: return candidate.startsWith(criteria);
324      case SemVer: return VersionUtilities.isSemVer(candidate) ? VersionUtilities.versionMatches(criteria, candidate) : false;
325      case Integer: return candidate.equals(criteria);
326      case Alpha: return candidate.startsWith(criteria);
327      case Date:return candidate.startsWith(criteria);
328      case Natural: return candidate.startsWith(criteria);
329      default: return candidate.startsWith(criteria);
330    }
331  }
332
333  protected FHIRException failWithIssue(IssueType type, OpIssueCode code, String path, String msgId, Object... params) {
334    String msg = context.formatMessage(msgId, params);
335    List<OperationOutcomeIssueComponent> issues = new ArrayList<>();
336    issues.addAll(makeIssue(IssueSeverity.ERROR, type, path, msg,  code, null, msgId));
337    throw new VSCheckerException(msg, issues, TerminologyServiceErrorClass.PROCESSING);
338  }
339
340  protected FHIRException fail(String msgId, Object... params) {
341    String msg = context.formatMessage(msgId, params);
342    allErrors.add(msg);
343    return new FHIRException(msg);
344  }
345
346  protected ValueSetProcessBase.UnknownValueSetException failWithUnknownVSException(String msgId, boolean check, Object... params) {
347    String msg = context.formatMessage(msgId, params);
348    allErrors.add(msg);
349    return new ValueSetProcessBase.UnknownValueSetException(msg);
350  }
351
352  protected OperationIsTooCostly failAsTooCostly(String msg) {
353    allErrors.add(msg);
354    return new OperationIsTooCostly(msg);
355  }
356
357  protected TerminologyServiceException failTSE(String msg) {
358    allErrors.add(msg);
359    return new TerminologyServiceException(msg);
360  }
361
362  public Collection<? extends String> getAllErrors() {
363    return allErrors;
364  }
365
366  protected AlternateCodesProcessingRules altCodeParams = new AlternateCodesProcessingRules(false);
367  protected AlternateCodesProcessingRules allAltCodes = new AlternateCodesProcessingRules(true);
368}