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