001package org.hl7.fhir.r4.conformance; 002 003/*- 004 * #%L 005 * org.hl7.fhir.r4 006 * %% 007 * Copyright (C) 2014 - 2019 Health Level 7 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 023 024import java.io.File; 025import java.io.IOException; 026import java.net.URL; 027import java.net.URLConnection; 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034 035import org.hl7.fhir.exceptions.DefinitionException; 036import org.hl7.fhir.exceptions.FHIRFormatError; 037import org.hl7.fhir.r4.context.IWorkerContext; 038import org.hl7.fhir.r4.formats.IParser; 039import org.hl7.fhir.r4.model.Base; 040import org.hl7.fhir.r4.model.Coding; 041import org.hl7.fhir.r4.model.ElementDefinition; 042import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent; 043import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionConstraintComponent; 044import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionMappingComponent; 045import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionSlicingComponent; 046import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent; 047import org.hl7.fhir.r4.model.Enumerations.BindingStrength; 048import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; 049import org.hl7.fhir.r4.model.IntegerType; 050import org.hl7.fhir.r4.model.PrimitiveType; 051import org.hl7.fhir.r4.model.StringType; 052import org.hl7.fhir.r4.model.StructureDefinition; 053import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule; 054import org.hl7.fhir.r4.model.Type; 055import org.hl7.fhir.r4.model.ValueSet; 056import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent; 057import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; 058import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent; 059import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 060import org.hl7.fhir.r4.utils.DefinitionNavigator; 061import org.hl7.fhir.r4.utils.ToolingExtensions; 062import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 063import org.hl7.fhir.utilities.TextFile; 064import org.hl7.fhir.utilities.Utilities; 065import org.hl7.fhir.utilities.validation.ValidationMessage; 066import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 067 068/** 069 * A engine that generates difference analysis between two sets of structure 070 * definitions, typically from 2 different implementation guides. 071 * 072 * How this class works is that you create it with access to a bunch of underying 073 * resources that includes all the structure definitions from both implementation 074 * guides 075 * 076 * Once the class is created, you repeatedly pass pairs of structure definitions, 077 * one from each IG, building up a web of difference analyses. This class will 078 * automatically process any internal comparisons that it encounters 079 * 080 * When all the comparisons have been performed, you can then generate a variety 081 * of output formats 082 * 083 * @author Grahame Grieve 084 * 085 */ 086public class ProfileComparer { 087 088 private IWorkerContext context; 089 090 public ProfileComparer(IWorkerContext context) { 091 super(); 092 this.context = context; 093 } 094 095 private static final int BOTH_NULL = 0; 096 private static final int EITHER_NULL = 1; 097 098 public class ProfileComparison { 099 private String id; 100 /** 101 * the first of two structures that were compared to generate this comparison 102 * 103 * In a few cases - selection of example content and value sets - left gets 104 * preference over right 105 */ 106 private StructureDefinition left; 107 108 /** 109 * the second of two structures that were compared to generate this comparison 110 * 111 * In a few cases - selection of example content and value sets - left gets 112 * preference over right 113 */ 114 private StructureDefinition right; 115 116 117 public String getId() { 118 return id; 119 } 120 private String leftName() { 121 return left.getName(); 122 } 123 private String rightName() { 124 return right.getName(); 125 } 126 127 /** 128 * messages generated during the comparison. There are 4 grades of messages: 129 * information - a list of differences between structures 130 * warnings - notifies that the comparer is unable to fully compare the structures (constraints differ, open value sets) 131 * errors - where the structures are incompatible 132 * fatal errors - some error that prevented full analysis 133 * 134 * @return 135 */ 136 private List<ValidationMessage> messages = new ArrayList<ValidationMessage>(); 137 138 /** 139 * The structure that describes all instances that will conform to both structures 140 */ 141 private StructureDefinition subset; 142 143 /** 144 * The structure that describes all instances that will conform to either structures 145 */ 146 private StructureDefinition superset; 147 148 public StructureDefinition getLeft() { 149 return left; 150 } 151 152 public StructureDefinition getRight() { 153 return right; 154 } 155 156 public List<ValidationMessage> getMessages() { 157 return messages; 158 } 159 160 public StructureDefinition getSubset() { 161 return subset; 162 } 163 164 public StructureDefinition getSuperset() { 165 return superset; 166 } 167 168 private boolean ruleEqual(String path, ElementDefinition ed, String vLeft, String vRight, String description, boolean nullOK) { 169 if (vLeft == null && vRight == null && nullOK) 170 return true; 171 if (vLeft == null && vRight == null) { 172 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, description+" and not null (null/null)", ValidationMessage.IssueSeverity.ERROR)); 173 if (ed != null) 174 status(ed, ProfileUtilities.STATUS_ERROR); 175 } 176 if (vLeft == null || !vLeft.equals(vRight)) { 177 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, description+" ("+vLeft+"/"+vRight+")", ValidationMessage.IssueSeverity.ERROR)); 178 if (ed != null) 179 status(ed, ProfileUtilities.STATUS_ERROR); 180 } 181 return true; 182 } 183 184 private boolean ruleCompares(ElementDefinition ed, Type vLeft, Type vRight, String path, int nullStatus) throws IOException { 185 if (vLeft == null && vRight == null && nullStatus == BOTH_NULL) 186 return true; 187 if (vLeft == null && vRight == null) { 188 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Must be the same and not null (null/null)", ValidationMessage.IssueSeverity.ERROR)); 189 status(ed, ProfileUtilities.STATUS_ERROR); 190 } 191 if (vLeft == null && nullStatus == EITHER_NULL) 192 return true; 193 if (vRight == null && nullStatus == EITHER_NULL) 194 return true; 195 if (vLeft == null || vRight == null || !Base.compareDeep(vLeft, vRight, false)) { 196 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Must be the same ("+toString(vLeft)+"/"+toString(vRight)+")", ValidationMessage.IssueSeverity.ERROR)); 197 status(ed, ProfileUtilities.STATUS_ERROR); 198 } 199 return true; 200 } 201 202 private boolean rule(ElementDefinition ed, boolean test, String path, String message) { 203 if (!test) { 204 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, message, ValidationMessage.IssueSeverity.ERROR)); 205 status(ed, ProfileUtilities.STATUS_ERROR); 206 } 207 return test; 208 } 209 210 private boolean ruleEqual(ElementDefinition ed, boolean vLeft, boolean vRight, String path, String elementName) { 211 if (vLeft != vRight) { 212 messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, elementName+" must be the same ("+vLeft+"/"+vRight+")", ValidationMessage.IssueSeverity.ERROR)); 213 status(ed, ProfileUtilities.STATUS_ERROR); 214 } 215 return true; 216 } 217 218 private String toString(Type val) throws IOException { 219 if (val instanceof PrimitiveType) 220 return "\"" + ((PrimitiveType) val).getValueAsString()+"\""; 221 222 IParser jp = context.newJsonParser(); 223 return jp.composeString(val, "value"); 224 } 225 226 public String getErrorCount() { 227 int c = 0; 228 for (ValidationMessage vm : messages) 229 if (vm.getLevel() == ValidationMessage.IssueSeverity.ERROR) 230 c++; 231 return Integer.toString(c); 232 } 233 234 public String getWarningCount() { 235 int c = 0; 236 for (ValidationMessage vm : messages) 237 if (vm.getLevel() == ValidationMessage.IssueSeverity.WARNING) 238 c++; 239 return Integer.toString(c); 240 } 241 242 public String getHintCount() { 243 int c = 0; 244 for (ValidationMessage vm : messages) 245 if (vm.getLevel() == ValidationMessage.IssueSeverity.INFORMATION) 246 c++; 247 return Integer.toString(c); 248 } 249 } 250 251 /** 252 * Value sets used in the subset and superset 253 */ 254 private List<ValueSet> valuesets = new ArrayList<ValueSet>(); 255 private List<ProfileComparison> comparisons = new ArrayList<ProfileComparison>(); 256 private String id; 257 private String title; 258 private String leftLink; 259 private String leftName; 260 private String rightLink; 261 private String rightName; 262 263 264 public List<ValueSet> getValuesets() { 265 return valuesets; 266 } 267 268 public void status(ElementDefinition ed, int value) { 269 ed.setUserData(ProfileUtilities.UD_ERROR_STATUS, Math.max(value, ed.getUserInt("error-status"))); 270 } 271 272 public List<ProfileComparison> getComparisons() { 273 return comparisons; 274 } 275 276 /** 277 * Compare left and right structure definitions to see whether they are consistent or not 278 * 279 * Note that left and right are arbitrary choices. In one respect, left 280 * is 'preferred' - the left's example value and data sets will be selected 281 * over the right ones in the common structure definition 282 * @throws DefinitionException 283 * @throws IOException 284 * @throws FHIRFormatError 285 * 286 * @ 287 */ 288 public ProfileComparison compareProfiles(StructureDefinition left, StructureDefinition right) throws DefinitionException, IOException, FHIRFormatError { 289 ProfileComparison outcome = new ProfileComparison(); 290 outcome.left = left; 291 outcome.right = right; 292 293 if (left == null) 294 throw new DefinitionException("No StructureDefinition provided (left)"); 295 if (right == null) 296 throw new DefinitionException("No StructureDefinition provided (right)"); 297 if (!left.hasSnapshot()) 298 throw new DefinitionException("StructureDefinition has no snapshot (left: "+outcome.leftName()+")"); 299 if (!right.hasSnapshot()) 300 throw new DefinitionException("StructureDefinition has no snapshot (right: "+outcome.rightName()+")"); 301 if (left.getSnapshot().getElement().isEmpty()) 302 throw new DefinitionException("StructureDefinition snapshot is empty (left: "+outcome.leftName()+")"); 303 if (right.getSnapshot().getElement().isEmpty()) 304 throw new DefinitionException("StructureDefinition snapshot is empty (right: "+outcome.rightName()+")"); 305 306 for (ProfileComparison pc : comparisons) 307 if (pc.left.getUrl().equals(left.getUrl()) && pc.right.getUrl().equals(right.getUrl())) 308 return pc; 309 310 outcome.id = Integer.toString(comparisons.size()+1); 311 comparisons.add(outcome); 312 313 DefinitionNavigator ln = new DefinitionNavigator(context, left); 314 DefinitionNavigator rn = new DefinitionNavigator(context, right); 315 316 // from here on in, any issues go in messages 317 outcome.superset = new StructureDefinition(); 318 outcome.subset = new StructureDefinition(); 319 if (outcome.ruleEqual(ln.path(), null,ln.path(), rn.path(), "Base Type is not compatible", false)) { 320 if (compareElements(outcome, ln.path(), ln, rn)) { 321 outcome.subset.setName("intersection of "+outcome.leftName()+" and "+outcome.rightName()); 322 outcome.subset.setStatus(PublicationStatus.DRAFT); 323 outcome.subset.setKind(outcome.left.getKind()); 324 outcome.subset.setType(outcome.left.getType()); 325 outcome.subset.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/"+outcome.subset.getType()); 326 outcome.subset.setDerivation(TypeDerivationRule.CONSTRAINT); 327 outcome.subset.setAbstract(false); 328 outcome.superset.setName("union of "+outcome.leftName()+" and "+outcome.rightName()); 329 outcome.superset.setStatus(PublicationStatus.DRAFT); 330 outcome.superset.setKind(outcome.left.getKind()); 331 outcome.superset.setType(outcome.left.getType()); 332 outcome.superset.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/"+outcome.subset.getType()); 333 outcome.superset.setAbstract(false); 334 outcome.superset.setDerivation(TypeDerivationRule.CONSTRAINT); 335 } else { 336 outcome.subset = null; 337 outcome.superset = null; 338 } 339 } 340 return outcome; 341 } 342 343 /** 344 * left and right refer to the same element. Are they compatible? 345 * @param outcome 346 * @param outcome 347 * @param path 348 * @param left 349 * @param right 350 * @- if there's a problem that needs fixing in this code 351 * @throws DefinitionException 352 * @throws IOException 353 * @throws FHIRFormatError 354 */ 355 private boolean compareElements(ProfileComparison outcome, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError { 356// preconditions: 357 assert(path != null); 358 assert(left != null); 359 assert(right != null); 360 assert(left.path().equals(right.path())); 361 362 // we ignore slicing right now - we're going to clone the root one anyway, and then think about clones 363 // simple stuff 364 ElementDefinition subset = new ElementDefinition(); 365 subset.setPath(left.path()); 366 367 // not allowed to be different: 368 subset.getRepresentation().addAll(left.current().getRepresentation()); // can't be bothered even testing this one 369 if (!outcome.ruleCompares(subset, left.current().getDefaultValue(), right.current().getDefaultValue(), path+".defaultValue[x]", BOTH_NULL)) 370 return false; 371 subset.setDefaultValue(left.current().getDefaultValue()); 372 if (!outcome.ruleEqual(path, subset, left.current().getMeaningWhenMissing(), right.current().getMeaningWhenMissing(), "meaningWhenMissing Must be the same", true)) 373 return false; 374 subset.setMeaningWhenMissing(left.current().getMeaningWhenMissing()); 375 if (!outcome.ruleEqual(subset, left.current().getIsModifier(), right.current().getIsModifier(), path, "isModifier")) 376 return false; 377 subset.setIsModifier(left.current().getIsModifier()); 378 if (!outcome.ruleEqual(subset, left.current().getIsSummary(), right.current().getIsSummary(), path, "isSummary")) 379 return false; 380 subset.setIsSummary(left.current().getIsSummary()); 381 382 // descriptive properties from ElementDefinition - merge them: 383 subset.setLabel(mergeText(subset, outcome, path, "label", left.current().getLabel(), right.current().getLabel())); 384 subset.setShort(mergeText(subset, outcome, path, "short", left.current().getShort(), right.current().getShort())); 385 subset.setDefinition(mergeText(subset, outcome, path, "definition", left.current().getDefinition(), right.current().getDefinition())); 386 subset.setComment(mergeText(subset, outcome, path, "comments", left.current().getComment(), right.current().getComment())); 387 subset.setRequirements(mergeText(subset, outcome, path, "requirements", left.current().getRequirements(), right.current().getRequirements())); 388 subset.getCode().addAll(mergeCodings(left.current().getCode(), right.current().getCode())); 389 subset.getAlias().addAll(mergeStrings(left.current().getAlias(), right.current().getAlias())); 390 subset.getMapping().addAll(mergeMappings(left.current().getMapping(), right.current().getMapping())); 391 // left will win for example 392 subset.setExample(left.current().hasExample() ? left.current().getExample() : right.current().getExample()); 393 394 subset.setMustSupport(left.current().getMustSupport() || right.current().getMustSupport()); 395 ElementDefinition superset = subset.copy(); 396 397 398 // compare and intersect 399 superset.setMin(unionMin(left.current().getMin(), right.current().getMin())); 400 superset.setMax(unionMax(left.current().getMax(), right.current().getMax())); 401 subset.setMin(intersectMin(left.current().getMin(), right.current().getMin())); 402 subset.setMax(intersectMax(left.current().getMax(), right.current().getMax())); 403 outcome.rule(subset, subset.getMax().equals("*") || Integer.parseInt(subset.getMax()) >= subset.getMin(), path, "Cardinality Mismatch: "+card(left)+"/"+card(right)); 404 405 superset.getType().addAll(unionTypes(path, left.current().getType(), right.current().getType())); 406 subset.getType().addAll(intersectTypes(subset, outcome, path, left.current().getType(), right.current().getType())); 407 outcome.rule(subset, !subset.getType().isEmpty() || (!left.current().hasType() && !right.current().hasType()), path, "Type Mismatch:\r\n "+typeCode(left)+"\r\n "+typeCode(right)); 408// <fixed[x]><!-- ?? 0..1 * Value must be exactly this --></fixed[x]> 409// <pattern[x]><!-- ?? 0..1 * Value must have at least these property values --></pattern[x]> 410 superset.setMaxLengthElement(unionMaxLength(left.current().getMaxLength(), right.current().getMaxLength())); 411 subset.setMaxLengthElement(intersectMaxLength(left.current().getMaxLength(), right.current().getMaxLength())); 412 if (left.current().hasBinding() || right.current().hasBinding()) { 413 compareBindings(outcome, subset, superset, path, left.current(), right.current()); 414 } 415 416 // note these are backwards 417 superset.getConstraint().addAll(intersectConstraints(path, left.current().getConstraint(), right.current().getConstraint())); 418 subset.getConstraint().addAll(unionConstraints(subset, outcome, path, left.current().getConstraint(), right.current().getConstraint())); 419 420 // now process the slices 421 if (left.current().hasSlicing() || right.current().hasSlicing()) { 422 if (isExtension(left.path())) 423 return compareExtensions(outcome, path, superset, subset, left, right); 424// return true; 425 else { 426 ElementDefinitionSlicingComponent slicingL = left.current().getSlicing(); 427 ElementDefinitionSlicingComponent slicingR = right.current().getSlicing(); 428 throw new DefinitionException("Slicing is not handled yet"); 429 } 430 // todo: name 431 } 432 433 // add the children 434 outcome.subset.getSnapshot().getElement().add(subset); 435 outcome.superset.getSnapshot().getElement().add(superset); 436 return compareChildren(subset, outcome, path, left, right); 437 } 438 439 private class ExtensionUsage { 440 private DefinitionNavigator defn; 441 private int minSuperset; 442 private int minSubset; 443 private String maxSuperset; 444 private String maxSubset; 445 private boolean both = false; 446 447 public ExtensionUsage(DefinitionNavigator defn, int min, String max) { 448 super(); 449 this.defn = defn; 450 this.minSubset = min; 451 this.minSuperset = min; 452 this.maxSubset = max; 453 this.maxSuperset = max; 454 } 455 456 } 457 private boolean compareExtensions(ProfileComparison outcome, String path, ElementDefinition superset, ElementDefinition subset, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException { 458 // for now, we don't handle sealed (or ordered) extensions 459 460 // for an extension the superset is all extensions, and the subset is.. all extensions - well, unless thay are sealed. 461 // but it's not useful to report that. instead, we collate the defined ones, and just adjust the cardinalities 462 Map<String, ExtensionUsage> map = new HashMap<String, ExtensionUsage>(); 463 464 if (left.slices() != null) 465 for (DefinitionNavigator ex : left.slices()) { 466 String url = ex.current().getType().get(0).getProfile().get(0).getValue(); 467 if (map.containsKey(url)) 468 throw new DefinitionException("Duplicate Extension "+url+" at "+path); 469 else 470 map.put(url, new ExtensionUsage(ex, ex.current().getMin(), ex.current().getMax())); 471 } 472 if (right.slices() != null) 473 for (DefinitionNavigator ex : right.slices()) { 474 String url = ex.current().getType().get(0).getProfile().get(0).getValue(); 475 if (map.containsKey(url)) { 476 ExtensionUsage exd = map.get(url); 477 exd.minSuperset = unionMin(exd.defn.current().getMin(), ex.current().getMin()); 478 exd.maxSuperset = unionMax(exd.defn.current().getMax(), ex.current().getMax()); 479 exd.minSubset = intersectMin(exd.defn.current().getMin(), ex.current().getMin()); 480 exd.maxSubset = intersectMax(exd.defn.current().getMax(), ex.current().getMax()); 481 exd.both = true; 482 outcome.rule(subset, exd.maxSubset.equals("*") || Integer.parseInt(exd.maxSubset) >= exd.minSubset, path, "Cardinality Mismatch on extension: "+card(exd.defn)+"/"+card(ex)); 483 } else { 484 map.put(url, new ExtensionUsage(ex, ex.current().getMin(), ex.current().getMax())); 485 } 486 } 487 List<String> names = new ArrayList<String>(); 488 names.addAll(map.keySet()); 489 Collections.sort(names); 490 for (String name : names) { 491 ExtensionUsage exd = map.get(name); 492 if (exd.both) 493 outcome.subset.getSnapshot().getElement().add(exd.defn.current().copy().setMin(exd.minSubset).setMax(exd.maxSubset)); 494 outcome.superset.getSnapshot().getElement().add(exd.defn.current().copy().setMin(exd.minSuperset).setMax(exd.maxSuperset)); 495 } 496 return true; 497 } 498 499 private boolean isExtension(String path) { 500 return path.endsWith(".extension") || path.endsWith(".modifierExtension"); 501 } 502 503 private boolean compareChildren(ElementDefinition ed, ProfileComparison outcome, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError { 504 List<DefinitionNavigator> lc = left.children(); 505 List<DefinitionNavigator> rc = right.children(); 506 // it's possible that one of these profiles walks into a data type and the other doesn't 507 // if it does, we have to load the children for that data into the profile that doesn't 508 // walk into it 509 if (lc.isEmpty() && !rc.isEmpty() && right.current().getType().size() == 1 && left.hasTypeChildren(right.current().getType().get(0))) 510 lc = left.childrenFromType(right.current().getType().get(0)); 511 if (rc.isEmpty() && !lc.isEmpty() && left.current().getType().size() == 1 && right.hasTypeChildren(left.current().getType().get(0))) 512 rc = right.childrenFromType(left.current().getType().get(0)); 513 if (lc.size() != rc.size()) { 514 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Different number of children at "+path+" ("+Integer.toString(lc.size())+"/"+Integer.toString(rc.size())+")", ValidationMessage.IssueSeverity.ERROR)); 515 status(ed, ProfileUtilities.STATUS_ERROR); 516 return false; 517 } else { 518 for (int i = 0; i < lc.size(); i++) { 519 DefinitionNavigator l = lc.get(i); 520 DefinitionNavigator r = rc.get(i); 521 String cpath = comparePaths(l.path(), r.path(), path, l.nameTail(), r.nameTail()); 522 if (cpath != null) { 523 if (!compareElements(outcome, cpath, l, r)) 524 return false; 525 } else { 526 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Different path at "+path+"["+Integer.toString(i)+"] ("+l.path()+"/"+r.path()+")", ValidationMessage.IssueSeverity.ERROR)); 527 status(ed, ProfileUtilities.STATUS_ERROR); 528 return false; 529 } 530 } 531 } 532 return true; 533 } 534 535 private String comparePaths(String path1, String path2, String path, String tail1, String tail2) { 536 if (tail1.equals(tail2)) { 537 return path+"."+tail1; 538 } else if (tail1.endsWith("[x]") && tail2.startsWith(tail1.substring(0, tail1.length()-3))) { 539 return path+"."+tail1; 540 } else if (tail2.endsWith("[x]") && tail1.startsWith(tail2.substring(0, tail2.length()-3))) { 541 return path+"."+tail2; 542 } else 543 return null; 544 } 545 546 private boolean compareBindings(ProfileComparison outcome, ElementDefinition subset, ElementDefinition superset, String path, ElementDefinition lDef, ElementDefinition rDef) throws FHIRFormatError { 547 assert(lDef.hasBinding() || rDef.hasBinding()); 548 if (!lDef.hasBinding()) { 549 subset.setBinding(rDef.getBinding()); 550 // technically, the super set is unbound, but that's not very useful - so we use the provided on as an example 551 superset.setBinding(rDef.getBinding().copy()); 552 superset.getBinding().setStrength(BindingStrength.EXAMPLE); 553 return true; 554 } 555 if (!rDef.hasBinding()) { 556 subset.setBinding(lDef.getBinding()); 557 superset.setBinding(lDef.getBinding().copy()); 558 superset.getBinding().setStrength(BindingStrength.EXAMPLE); 559 return true; 560 } 561 ElementDefinitionBindingComponent left = lDef.getBinding(); 562 ElementDefinitionBindingComponent right = rDef.getBinding(); 563 if (Base.compareDeep(left, right, false)) { 564 subset.setBinding(left); 565 superset.setBinding(right); 566 } 567 568 // if they're both examples/preferred then: 569 // subset: left wins if they're both the same 570 // superset: 571 if (isPreferredOrExample(left) && isPreferredOrExample(right)) { 572 if (right.getStrength() == BindingStrength.PREFERRED && left.getStrength() == BindingStrength.EXAMPLE && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { 573 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Example/preferred bindings differ at "+path+" using binding from "+outcome.rightName(), ValidationMessage.IssueSeverity.INFORMATION)); 574 status(subset, ProfileUtilities.STATUS_HINT); 575 subset.setBinding(right); 576 superset.setBinding(unionBindings(superset, outcome, path, left, right)); 577 } else { 578 if ((right.getStrength() != BindingStrength.EXAMPLE || left.getStrength() != BindingStrength.EXAMPLE) && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false) ) { 579 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Example/preferred bindings differ at "+path+" using binding from "+outcome.leftName(), ValidationMessage.IssueSeverity.INFORMATION)); 580 status(subset, ProfileUtilities.STATUS_HINT); 581 } 582 subset.setBinding(left); 583 superset.setBinding(unionBindings(superset, outcome, path, left, right)); 584 } 585 return true; 586 } 587 // if either of them are extensible/required, then it wins 588 if (isPreferredOrExample(left)) { 589 subset.setBinding(right); 590 superset.setBinding(unionBindings(superset, outcome, path, left, right)); 591 return true; 592 } 593 if (isPreferredOrExample(right)) { 594 subset.setBinding(left); 595 superset.setBinding(unionBindings(superset, outcome, path, left, right)); 596 return true; 597 } 598 599 // ok, both are extensible or required. 600 ElementDefinitionBindingComponent subBinding = new ElementDefinitionBindingComponent(); 601 subset.setBinding(subBinding); 602 ElementDefinitionBindingComponent superBinding = new ElementDefinitionBindingComponent(); 603 superset.setBinding(superBinding); 604 subBinding.setDescription(mergeText(subset, outcome, path, "description", left.getDescription(), right.getDescription())); 605 superBinding.setDescription(mergeText(subset, outcome, null, "description", left.getDescription(), right.getDescription())); 606 if (left.getStrength() == BindingStrength.REQUIRED || right.getStrength() == BindingStrength.REQUIRED) 607 subBinding.setStrength(BindingStrength.REQUIRED); 608 else 609 subBinding.setStrength(BindingStrength.EXTENSIBLE); 610 if (left.getStrength() == BindingStrength.EXTENSIBLE || right.getStrength() == BindingStrength.EXTENSIBLE) 611 superBinding.setStrength(BindingStrength.EXTENSIBLE); 612 else 613 superBinding.setStrength(BindingStrength.REQUIRED); 614 615 if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { 616 subBinding.setValueSet(left.getValueSet()); 617 superBinding.setValueSet(left.getValueSet()); 618 return true; 619 } else if (!left.hasValueSet()) { 620 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "No left Value set at "+path, ValidationMessage.IssueSeverity.ERROR)); 621 return true; 622 } else if (!right.hasValueSet()) { 623 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "No right Value set at "+path, ValidationMessage.IssueSeverity.ERROR)); 624 return true; 625 } else { 626 // ok, now we compare the value sets. This may be unresolvable. 627 ValueSet lvs = resolveVS(outcome.left, left.getValueSet()); 628 ValueSet rvs = resolveVS(outcome.right, right.getValueSet()); 629 if (lvs == null) { 630 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to resolve left value set "+left.getValueSet().toString()+" at "+path, ValidationMessage.IssueSeverity.ERROR)); 631 return true; 632 } else if (rvs == null) { 633 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to resolve right value set "+right.getValueSet().toString()+" at "+path, ValidationMessage.IssueSeverity.ERROR)); 634 return true; 635 } else { 636 // first, we'll try to do it by definition 637 ValueSet cvs = intersectByDefinition(lvs, rvs); 638 if(cvs == null) { 639 // if that didn't work, we'll do it by expansion 640 ValueSetExpansionOutcome le; 641 ValueSetExpansionOutcome re; 642 try { 643 le = context.expandVS(lvs, true, false); 644 re = context.expandVS(rvs, true, false); 645 if (!closed(le.getValueset()) || !closed(re.getValueset())) 646 throw new DefinitionException("unclosed value sets are not handled yet"); 647 cvs = intersectByExpansion(lvs, rvs); 648 if (!cvs.getCompose().hasInclude()) { 649 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value sets "+lvs.getUrl()+" and "+rvs.getUrl()+" do not intersect", ValidationMessage.IssueSeverity.ERROR)); 650 status(subset, ProfileUtilities.STATUS_ERROR); 651 return false; 652 } 653 } catch (Exception e){ 654 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to expand or process value sets "+lvs.getUrl()+" and "+rvs.getUrl()+": "+e.getMessage(), ValidationMessage.IssueSeverity.ERROR)); 655 status(subset, ProfileUtilities.STATUS_ERROR); 656 return false; 657 } 658 } 659 subBinding.setValueSet("#"+addValueSet(cvs)); 660 superBinding.setValueSet("#"+addValueSet(unite(superset, outcome, path, lvs, rvs))); 661 } 662 } 663 return false; 664 } 665 666 private ElementDefinitionBindingComponent unionBindings(ElementDefinition ed, ProfileComparison outcome, String path, ElementDefinitionBindingComponent left, ElementDefinitionBindingComponent right) throws FHIRFormatError { 667 ElementDefinitionBindingComponent union = new ElementDefinitionBindingComponent(); 668 if (left.getStrength().compareTo(right.getStrength()) < 0) 669 union.setStrength(left.getStrength()); 670 else 671 union.setStrength(right.getStrength()); 672 union.setDescription(mergeText(ed, outcome, path, "binding.description", left.getDescription(), right.getDescription())); 673 if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) 674 union.setValueSet(left.getValueSet()); 675 else { 676 ValueSet lvs = resolveVS(outcome.left, left.getValueSet()); 677 ValueSet rvs = resolveVS(outcome.left, right.getValueSet()); 678 if (lvs != null && rvs != null) 679 union.setValueSet("#"+addValueSet(unite(ed, outcome, path, lvs, rvs))); 680 else if (lvs != null) 681 union.setValueSet("#"+addValueSet(lvs)); 682 else if (rvs != null) 683 union.setValueSet("#"+addValueSet(rvs)); 684 } 685 return union; 686 } 687 688 689 private ValueSet unite(ElementDefinition ed, ProfileComparison outcome, String path, ValueSet lvs, ValueSet rvs) { 690 ValueSet vs = new ValueSet(); 691 if (lvs.hasCompose()) { 692 for (ConceptSetComponent inc : lvs.getCompose().getInclude()) 693 vs.getCompose().getInclude().add(inc); 694 if (lvs.getCompose().hasExclude()) { 695 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value sets "+lvs.getUrl()+" has exclude statements, and no union involving it can be correctly determined", ValidationMessage.IssueSeverity.ERROR)); 696 status(ed, ProfileUtilities.STATUS_ERROR); 697 } 698 } 699 if (rvs.hasCompose()) { 700 for (ConceptSetComponent inc : rvs.getCompose().getInclude()) 701 if (!mergeIntoExisting(vs.getCompose().getInclude(), inc)) 702 vs.getCompose().getInclude().add(inc); 703 if (rvs.getCompose().hasExclude()) { 704 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value sets "+lvs.getUrl()+" has exclude statements, and no union involving it can be correctly determined", ValidationMessage.IssueSeverity.ERROR)); 705 status(ed, ProfileUtilities.STATUS_ERROR); 706 } 707 } 708 return vs; 709 } 710 711 private boolean mergeIntoExisting(List<ConceptSetComponent> include, ConceptSetComponent inc) { 712 for (ConceptSetComponent dst : include) { 713 if (Base.compareDeep(dst, inc, false)) 714 return true; // they're actually the same 715 if (dst.getSystem().equals(inc.getSystem())) { 716 if (inc.hasFilter() || dst.hasFilter()) { 717 return false; // just add the new one as a a parallel 718 } else if (inc.hasConcept() && dst.hasConcept()) { 719 for (ConceptReferenceComponent cc : inc.getConcept()) { 720 boolean found = false; 721 for (ConceptReferenceComponent dd : dst.getConcept()) { 722 if (dd.getCode().equals(cc.getCode())) 723 found = true; 724 if (found) { 725 if (cc.hasDisplay() && !dd.hasDisplay()) 726 dd.setDisplay(cc.getDisplay()); 727 break; 728 } 729 } 730 if (!found) 731 dst.getConcept().add(cc.copy()); 732 } 733 } else 734 dst.getConcept().clear(); // one of them includes the entire code system 735 } 736 } 737 return false; 738 } 739 740 private ValueSet resolveVS(StructureDefinition ctxtLeft, String vsRef) { 741 if (vsRef == null) 742 return null; 743 return context.fetchResource(ValueSet.class, vsRef); 744 } 745 746 private ValueSet intersectByDefinition(ValueSet lvs, ValueSet rvs) { 747 // this is just a stub. The idea is that we try to avoid expanding big open value sets from SCT, RxNorm, LOINC. 748 // there's a bit of long hand logic coming here, but that's ok. 749 return null; 750 } 751 752 private ValueSet intersectByExpansion(ValueSet lvs, ValueSet rvs) { 753 // this is pretty straight forward - we intersect the lists, and build a compose out of the intersection 754 ValueSet vs = new ValueSet(); 755 vs.setStatus(PublicationStatus.DRAFT); 756 757 Map<String, ValueSetExpansionContainsComponent> left = new HashMap<String, ValueSetExpansionContainsComponent>(); 758 scan(lvs.getExpansion().getContains(), left); 759 Map<String, ValueSetExpansionContainsComponent> right = new HashMap<String, ValueSetExpansionContainsComponent>(); 760 scan(rvs.getExpansion().getContains(), right); 761 Map<String, ConceptSetComponent> inc = new HashMap<String, ConceptSetComponent>(); 762 763 for (String s : left.keySet()) { 764 if (right.containsKey(s)) { 765 ValueSetExpansionContainsComponent cc = left.get(s); 766 ConceptSetComponent c = inc.get(cc.getSystem()); 767 if (c == null) { 768 c = vs.getCompose().addInclude().setSystem(cc.getSystem()); 769 inc.put(cc.getSystem(), c); 770 } 771 c.addConcept().setCode(cc.getCode()).setDisplay(cc.getDisplay()); 772 } 773 } 774 return vs; 775 } 776 777 private void scan(List<ValueSetExpansionContainsComponent> list, Map<String, ValueSetExpansionContainsComponent> map) { 778 for (ValueSetExpansionContainsComponent cc : list) { 779 if (cc.hasSystem() && cc.hasCode()) { 780 String s = cc.getSystem()+"::"+cc.getCode(); 781 if (!map.containsKey(s)) 782 map.put(s, cc); 783 } 784 if (cc.hasContains()) 785 scan(cc.getContains(), map); 786 } 787 } 788 789 private boolean closed(ValueSet vs) { 790 return !ToolingExtensions.findBooleanExtension(vs.getExpansion(), ToolingExtensions.EXT_UNCLOSED); 791 } 792 793 private boolean isPreferredOrExample(ElementDefinitionBindingComponent binding) { 794 return binding.getStrength() == BindingStrength.EXAMPLE || binding.getStrength() == BindingStrength.PREFERRED; 795 } 796 797 private Collection<? extends TypeRefComponent> intersectTypes(ElementDefinition ed, ProfileComparison outcome, String path, List<TypeRefComponent> left, List<TypeRefComponent> right) throws DefinitionException, IOException, FHIRFormatError { 798 List<TypeRefComponent> result = new ArrayList<TypeRefComponent>(); 799 for (TypeRefComponent l : left) { 800 if (l.hasAggregation()) 801 throw new DefinitionException("Aggregation not supported: "+path); 802 boolean pfound = false; 803 boolean tfound = false; 804 TypeRefComponent c = l.copy(); 805 for (TypeRefComponent r : right) { 806 if (r.hasAggregation()) 807 throw new DefinitionException("Aggregation not supported: "+path); 808 if (!l.hasProfile() && !r.hasProfile()) { 809 pfound = true; 810 } else if (!r.hasProfile()) { 811 pfound = true; 812 } else if (!l.hasProfile()) { 813 pfound = true; 814 c.setProfile(r.getProfile()); 815 } else { 816 StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), outcome.leftName()); 817 StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), outcome.rightName()); 818 if (sdl != null && sdr != null) { 819 if (sdl == sdr) { 820 pfound = true; 821 } else if (derivesFrom(sdl, sdr)) { 822 pfound = true; 823 } else if (derivesFrom(sdr, sdl)) { 824 c.setProfile(r.getProfile()); 825 pfound = true; 826 } else if (sdl.getType().equals(sdr.getType())) { 827 ProfileComparison comp = compareProfiles(sdl, sdr); 828 if (comp.getSubset() != null) { 829 pfound = true; 830 c.addProfile("#"+comp.id); 831 } 832 } 833 } 834 } 835 if (!l.hasTargetProfile() && !r.hasTargetProfile()) { 836 tfound = true; 837 } else if (!r.hasTargetProfile()) { 838 tfound = true; 839 } else if (!l.hasTargetProfile()) { 840 tfound = true; 841 c.setTargetProfile(r.getTargetProfile()); 842 } else { 843 StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), outcome.leftName()); 844 StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), outcome.rightName()); 845 if (sdl != null && sdr != null) { 846 if (sdl == sdr) { 847 tfound = true; 848 } else if (derivesFrom(sdl, sdr)) { 849 tfound = true; 850 } else if (derivesFrom(sdr, sdl)) { 851 c.setTargetProfile(r.getTargetProfile()); 852 tfound = true; 853 } else if (sdl.getType().equals(sdr.getType())) { 854 ProfileComparison comp = compareProfiles(sdl, sdr); 855 if (comp.getSubset() != null) { 856 tfound = true; 857 c.addTargetProfile("#"+comp.id); 858 } 859 } 860 } 861 } 862 } 863 if (pfound && tfound) 864 result.add(c); 865 } 866 return result; 867 } 868 869 private StructureDefinition resolveProfile(ElementDefinition ed, ProfileComparison outcome, String path, String url, String name) { 870 StructureDefinition res = context.fetchResource(StructureDefinition.class, url); 871 if (res == null) { 872 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, path, "Unable to resolve profile "+url+" in profile "+name, ValidationMessage.IssueSeverity.WARNING)); 873 status(ed, ProfileUtilities.STATUS_HINT); 874 } 875 return res; 876 } 877 878 private Collection<? extends TypeRefComponent> unionTypes(String path, List<TypeRefComponent> left, List<TypeRefComponent> right) throws DefinitionException, IOException, FHIRFormatError { 879 List<TypeRefComponent> result = new ArrayList<TypeRefComponent>(); 880 for (TypeRefComponent l : left) 881 checkAddTypeUnion(path, result, l); 882 for (TypeRefComponent r : right) 883 checkAddTypeUnion(path, result, r); 884 return result; 885 } 886 887 private void checkAddTypeUnion(String path, List<TypeRefComponent> results, TypeRefComponent nw) throws DefinitionException, IOException, FHIRFormatError { 888 boolean pfound = false; 889 boolean tfound = false; 890 nw = nw.copy(); 891 if (nw.hasAggregation()) 892 throw new DefinitionException("Aggregation not supported: "+path); 893 for (TypeRefComponent ex : results) { 894 if (Utilities.equals(ex.getWorkingCode(), nw.getWorkingCode())) { 895 if (!ex.hasProfile() && !nw.hasProfile()) 896 pfound = true; 897 else if (!ex.hasProfile()) { 898 pfound = true; 899 } else if (!nw.hasProfile()) { 900 pfound = true; 901 ex.setProfile(null); 902 } else { 903 // both have profiles. Is one derived from the other? 904 StructureDefinition sdex = context.fetchResource(StructureDefinition.class, ex.getProfile().get(0).getValue()); 905 StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, nw.getProfile().get(0).getValue()); 906 if (sdex != null && sdnw != null) { 907 if (sdex == sdnw) { 908 pfound = true; 909 } else if (derivesFrom(sdex, sdnw)) { 910 ex.setProfile(nw.getProfile()); 911 pfound = true; 912 } else if (derivesFrom(sdnw, sdex)) { 913 pfound = true; 914 } else if (sdnw.getSnapshot().getElement().get(0).getPath().equals(sdex.getSnapshot().getElement().get(0).getPath())) { 915 ProfileComparison comp = compareProfiles(sdex, sdnw); 916 if (comp.getSuperset() != null) { 917 pfound = true; 918 ex.addProfile("#"+comp.id); 919 } 920 } 921 } 922 } 923 if (!ex.hasTargetProfile() && !nw.hasTargetProfile()) 924 tfound = true; 925 else if (!ex.hasTargetProfile()) { 926 tfound = true; 927 } else if (!nw.hasTargetProfile()) { 928 tfound = true; 929 ex.setTargetProfile(null); 930 } else { 931 // both have profiles. Is one derived from the other? 932 StructureDefinition sdex = context.fetchResource(StructureDefinition.class, ex.getTargetProfile().get(0).getValue()); 933 StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, nw.getTargetProfile().get(0).getValue()); 934 if (sdex != null && sdnw != null) { 935 if (sdex == sdnw) { 936 tfound = true; 937 } else if (derivesFrom(sdex, sdnw)) { 938 ex.setTargetProfile(nw.getTargetProfile()); 939 tfound = true; 940 } else if (derivesFrom(sdnw, sdex)) { 941 tfound = true; 942 } else if (sdnw.getSnapshot().getElement().get(0).getPath().equals(sdex.getSnapshot().getElement().get(0).getPath())) { 943 ProfileComparison comp = compareProfiles(sdex, sdnw); 944 if (comp.getSuperset() != null) { 945 tfound = true; 946 ex.addTargetProfile("#"+comp.id); 947 } 948 } 949 } 950 } 951 } 952 } 953 if (!tfound || !pfound) 954 results.add(nw); 955 } 956 957 958 private boolean derivesFrom(StructureDefinition left, StructureDefinition right) { 959 // left derives from right if it's base is the same as right 960 // todo: recursive... 961 return left.hasBaseDefinition() && left.getBaseDefinition().equals(right.getUrl()); 962 } 963 964 965 private String mergeText(ElementDefinition ed, ProfileComparison outcome, String path, String name, String left, String right) { 966 if (left == null && right == null) 967 return null; 968 if (left == null) 969 return right; 970 if (right == null) 971 return left; 972 if (left.equalsIgnoreCase(right)) 973 return left; 974 if (path != null) { 975 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, path, "Elements differ in definition for "+name+":\r\n \""+left+"\"\r\n \""+right+"\"", 976 "Elements differ in definition for "+name+":<br/>\""+Utilities.escapeXml(left)+"\"<br/>\""+Utilities.escapeXml(right)+"\"", ValidationMessage.IssueSeverity.INFORMATION)); 977 status(ed, ProfileUtilities.STATUS_HINT); 978 } 979 return "left: "+left+"; right: "+right; 980 } 981 982 private List<Coding> mergeCodings(List<Coding> left, List<Coding> right) { 983 List<Coding> result = new ArrayList<Coding>(); 984 result.addAll(left); 985 for (Coding c : right) { 986 boolean found = false; 987 for (Coding ct : left) 988 if (Utilities.equals(c.getSystem(), ct.getSystem()) && Utilities.equals(c.getCode(), ct.getCode())) 989 found = true; 990 if (!found) 991 result.add(c); 992 } 993 return result; 994 } 995 996 private List<StringType> mergeStrings(List<StringType> left, List<StringType> right) { 997 List<StringType> result = new ArrayList<StringType>(); 998 result.addAll(left); 999 for (StringType c : right) { 1000 boolean found = false; 1001 for (StringType ct : left) 1002 if (Utilities.equals(c.getValue(), ct.getValue())) 1003 found = true; 1004 if (!found) 1005 result.add(c); 1006 } 1007 return result; 1008 } 1009 1010 private List<ElementDefinitionMappingComponent> mergeMappings(List<ElementDefinitionMappingComponent> left, List<ElementDefinitionMappingComponent> right) { 1011 List<ElementDefinitionMappingComponent> result = new ArrayList<ElementDefinitionMappingComponent>(); 1012 result.addAll(left); 1013 for (ElementDefinitionMappingComponent c : right) { 1014 boolean found = false; 1015 for (ElementDefinitionMappingComponent ct : left) 1016 if (Utilities.equals(c.getIdentity(), ct.getIdentity()) && Utilities.equals(c.getLanguage(), ct.getLanguage()) && Utilities.equals(c.getMap(), ct.getMap())) 1017 found = true; 1018 if (!found) 1019 result.add(c); 1020 } 1021 return result; 1022 } 1023 1024 // we can't really know about constraints. We create warnings, and collate them 1025 private List<ElementDefinitionConstraintComponent> unionConstraints(ElementDefinition ed, ProfileComparison outcome, String path, List<ElementDefinitionConstraintComponent> left, List<ElementDefinitionConstraintComponent> right) { 1026 List<ElementDefinitionConstraintComponent> result = new ArrayList<ElementDefinitionConstraintComponent>(); 1027 for (ElementDefinitionConstraintComponent l : left) { 1028 boolean found = false; 1029 for (ElementDefinitionConstraintComponent r : right) 1030 if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) 1031 found = true; 1032 if (!found) { 1033 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "StructureDefinition "+outcome.leftName()+" has a constraint that is not found in "+outcome.rightName()+" and it is uncertain whether they are compatible ("+l.getXpath()+")", ValidationMessage.IssueSeverity.INFORMATION)); 1034 status(ed, ProfileUtilities.STATUS_WARNING); 1035 } 1036 result.add(l); 1037 } 1038 for (ElementDefinitionConstraintComponent r : right) { 1039 boolean found = false; 1040 for (ElementDefinitionConstraintComponent l : left) 1041 if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) 1042 found = true; 1043 if (!found) { 1044 outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "StructureDefinition "+outcome.rightName()+" has a constraint that is not found in "+outcome.leftName()+" and it is uncertain whether they are compatible ("+r.getXpath()+")", ValidationMessage.IssueSeverity.INFORMATION)); 1045 status(ed, ProfileUtilities.STATUS_WARNING); 1046 result.add(r); 1047 } 1048 } 1049 return result; 1050 } 1051 1052 1053 private List<ElementDefinitionConstraintComponent> intersectConstraints(String path, List<ElementDefinitionConstraintComponent> left, List<ElementDefinitionConstraintComponent> right) { 1054 List<ElementDefinitionConstraintComponent> result = new ArrayList<ElementDefinitionConstraintComponent>(); 1055 for (ElementDefinitionConstraintComponent l : left) { 1056 boolean found = false; 1057 for (ElementDefinitionConstraintComponent r : right) 1058 if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) 1059 found = true; 1060 if (found) 1061 result.add(l); 1062 } 1063 return result; 1064} 1065 1066 private String card(DefinitionNavigator defn) { 1067 return Integer.toString(defn.current().getMin())+".."+defn.current().getMax(); 1068 } 1069 1070 private String typeCode(DefinitionNavigator defn) { 1071 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 1072 for (TypeRefComponent t : defn.current().getType()) 1073 b.append(t.getWorkingCode()+(t.hasProfile() ? "("+t.getProfile()+")" : "")+(t.hasTargetProfile() ? "("+t.getTargetProfile()+")" : "")); // todo: other properties 1074 return b.toString(); 1075 } 1076 1077 private int intersectMin(int left, int right) { 1078 if (left > right) 1079 return left; 1080 else 1081 return right; 1082 } 1083 1084 private int unionMin(int left, int right) { 1085 if (left > right) 1086 return right; 1087 else 1088 return left; 1089 } 1090 1091 private String intersectMax(String left, String right) { 1092 int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left); 1093 int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right); 1094 if (l < r) 1095 return left; 1096 else 1097 return right; 1098 } 1099 1100 private String unionMax(String left, String right) { 1101 int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left); 1102 int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right); 1103 if (l < r) 1104 return right; 1105 else 1106 return left; 1107 } 1108 1109 private IntegerType intersectMaxLength(int left, int right) { 1110 if (left == 0) 1111 left = Integer.MAX_VALUE; 1112 if (right == 0) 1113 right = Integer.MAX_VALUE; 1114 if (left < right) 1115 return left == Integer.MAX_VALUE ? null : new IntegerType(left); 1116 else 1117 return right == Integer.MAX_VALUE ? null : new IntegerType(right); 1118 } 1119 1120 private IntegerType unionMaxLength(int left, int right) { 1121 if (left == 0) 1122 left = Integer.MAX_VALUE; 1123 if (right == 0) 1124 right = Integer.MAX_VALUE; 1125 if (left < right) 1126 return right == Integer.MAX_VALUE ? null : new IntegerType(right); 1127 else 1128 return left == Integer.MAX_VALUE ? null : new IntegerType(left); 1129 } 1130 1131 1132 public String addValueSet(ValueSet cvs) { 1133 String id = Integer.toString(valuesets.size()+1); 1134 cvs.setId(id); 1135 valuesets.add(cvs); 1136 return id; 1137 } 1138 1139 1140 1141 public String getId() { 1142 return id; 1143 } 1144 1145 public void setId(String id) { 1146 this.id = id; 1147 } 1148 1149 public String getTitle() { 1150 return title; 1151 } 1152 1153 public void setTitle(String title) { 1154 this.title = title; 1155 } 1156 1157 public String getLeftLink() { 1158 return leftLink; 1159 } 1160 1161 public void setLeftLink(String leftLink) { 1162 this.leftLink = leftLink; 1163 } 1164 1165 public String getLeftName() { 1166 return leftName; 1167 } 1168 1169 public void setLeftName(String leftName) { 1170 this.leftName = leftName; 1171 } 1172 1173 public String getRightLink() { 1174 return rightLink; 1175 } 1176 1177 public void setRightLink(String rightLink) { 1178 this.rightLink = rightLink; 1179 } 1180 1181 public String getRightName() { 1182 return rightName; 1183 } 1184 1185 public void setRightName(String rightName) { 1186 this.rightName = rightName; 1187 } 1188 1189 private String genPCLink(String leftName, String leftLink) { 1190 return "<a href=\""+leftLink+"\">"+Utilities.escapeXml(leftName)+"</a>"; 1191 } 1192 1193 private String genPCTable() { 1194 StringBuilder b = new StringBuilder(); 1195 1196 b.append("<table class=\"grid\">\r\n"); 1197 b.append("<tr>"); 1198 b.append(" <td><b>Left</b></td>"); 1199 b.append(" <td><b>Right</b></td>"); 1200 b.append(" <td><b>Comparison</b></td>"); 1201 b.append(" <td><b>Error #</b></td>"); 1202 b.append(" <td><b>Warning #</b></td>"); 1203 b.append(" <td><b>Hint #</b></td>"); 1204 b.append("</tr>"); 1205 1206 for (ProfileComparison cmp : getComparisons()) { 1207 b.append("<tr>"); 1208 b.append(" <td><a href=\""+cmp.getLeft().getUserString("path")+"\">"+Utilities.escapeXml(cmp.getLeft().getName())+"</a></td>"); 1209 b.append(" <td><a href=\""+cmp.getRight().getUserString("path")+"\">"+Utilities.escapeXml(cmp.getRight().getName())+"</a></td>"); 1210 b.append(" <td><a href=\""+getId()+"."+cmp.getId()+".html\">Click Here</a></td>"); 1211 b.append(" <td>"+cmp.getErrorCount()+"</td>"); 1212 b.append(" <td>"+cmp.getWarningCount()+"</td>"); 1213 b.append(" <td>"+cmp.getHintCount()+"</td>"); 1214 b.append("</tr>"); 1215 } 1216 b.append("</table>\r\n"); 1217 1218 return b.toString(); 1219 } 1220 1221 1222 public String generate(String dest) throws IOException { 1223 // ok, all compared; now produce the output 1224 // first page we produce is simply the index 1225 Map<String, String> vars = new HashMap<String, String>(); 1226 vars.put("title", getTitle()); 1227 vars.put("left", genPCLink(getLeftName(), getLeftLink())); 1228 vars.put("right", genPCLink(getRightName(), getRightLink())); 1229 vars.put("table", genPCTable()); 1230 producePage(summaryTemplate(), Utilities.path(dest, getId()+".html"), vars); 1231 1232// page.log(" ... generate", LogMessageType.Process); 1233// String src = TextFile.fileToString(page.getFolders().srcDir + "template-comparison-set.html"); 1234// src = page.processPageIncludes(n+".html", src, "?type", null, "??path", null, null, "Comparison", pc, null, null, page.getDefinitions().getWorkgroups().get("fhir")); 1235// TextFile.stringToFile(src, Utilities.path(page.getFolders().dstDir, n+".html")); 1236// cachePage(n + ".html", src, "Comparison "+pc.getTitle(), false); 1237// 1238// // then we produce a comparison page for each pair 1239// for (ProfileComparison cmp : pc.getComparisons()) { 1240// src = TextFile.fileToString(page.getFolders().srcDir + "template-comparison.html"); 1241// src = page.processPageIncludes(n+"."+cmp.getId()+".html", src, "?type", null, "??path", null, null, "Comparison", cmp, null, null, page.getDefinitions().getWorkgroups().get("fhir")); 1242// TextFile.stringToFile(src, Utilities.path(page.getFolders().dstDir, n+"."+cmp.getId()+".html")); 1243// cachePage(n +"."+cmp.getId()+".html", src, "Comparison "+pc.getTitle(), false); 1244// } 1245// // and also individual pages for each pair outcome 1246// // then we produce value set pages for each value set 1247// 1248// // TODO Auto-generated method stub 1249 return Utilities.path(dest, getId()+".html"); 1250 } 1251 1252 private void producePage(String src, String path, Map<String, String> vars) throws IOException { 1253 while (src.contains("[%")) 1254 { 1255 int i1 = src.indexOf("[%"); 1256 int i2 = src.substring(i1).indexOf("%]")+i1; 1257 String s1 = src.substring(0, i1); 1258 String s2 = src.substring(i1 + 2, i2).trim(); 1259 String s3 = src.substring(i2+2); 1260 String v = vars.containsKey(s2) ? vars.get(s2) : "???"; 1261 src = s1+v+s3; 1262 } 1263 TextFile.stringToFile(src, path); 1264 } 1265 1266 private String summaryTemplate() throws IOException { 1267 return cachedFetch("04a9d69a-47f2-4250-8645-bf5d880a8eaa-1.fhir-template", "http://build.fhir.org/template-comparison-set.html.template"); 1268 } 1269 1270 private String cachedFetch(String id, String source) throws IOException { 1271 String tmpDir = System.getProperty("java.io.tmpdir"); 1272 String local = Utilities.path(tmpDir, id); 1273 File f = new File(local); 1274 if (f.exists()) 1275 return TextFile.fileToString(f); 1276 URL url = new URL(source); 1277 URLConnection c = url.openConnection(); 1278 String result = TextFile.streamToString(c.getInputStream()); 1279 TextFile.stringToFile(result, f); 1280 return result; 1281 } 1282 1283 1284 1285 1286}