001package ca.uhn.fhir.parser; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2019 University Health Network 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.*; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum; 025import ca.uhn.fhir.model.api.*; 026import ca.uhn.fhir.model.primitive.IdDt; 027import ca.uhn.fhir.rest.api.Constants; 028import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 029import ca.uhn.fhir.util.UrlUtil; 030import com.google.common.base.Charsets; 031import org.apache.commons.lang3.StringUtils; 032import org.apache.commons.lang3.Validate; 033import org.apache.commons.lang3.builder.EqualsBuilder; 034import org.apache.commons.lang3.builder.HashCodeBuilder; 035import org.hl7.fhir.instance.model.api.*; 036 037import javax.annotation.Nullable; 038import java.io.*; 039import java.lang.reflect.Modifier; 040import java.util.*; 041import java.util.stream.Collectors; 042 043import static org.apache.commons.lang3.StringUtils.isBlank; 044import static org.apache.commons.lang3.StringUtils.isNotBlank; 045 046@SuppressWarnings("WeakerAccess") 047public abstract class BaseParser implements IParser { 048 049 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParser.class); 050 051 private static final Set<String> notEncodeForContainedResource = new HashSet<>(Arrays.asList("security", "versionId", "lastUpdated")); 052 053 private ContainedResources myContainedResources; 054 private boolean myEncodeElementsAppliesToChildResourcesOnly; 055 private FhirContext myContext; 056 private List<ElementsPath> myDontEncodeElements; 057 private List<ElementsPath> myEncodeElements; 058 private Set<String> myEncodeElementsAppliesToResourceTypes; 059 private IIdType myEncodeForceResourceId; 060 private IParserErrorHandler myErrorHandler; 061 private boolean myOmitResourceId; 062 private List<Class<? extends IBaseResource>> myPreferTypes; 063 private String myServerBaseUrl; 064 private Boolean myStripVersionsFromReferences; 065 private Boolean myOverrideResourceIdWithBundleEntryFullUrl; 066 private boolean mySummaryMode; 067 private boolean mySuppressNarratives; 068 private Set<String> myDontStripVersionsFromReferencesAtPaths; 069 070 /** 071 * Constructor 072 */ 073 public BaseParser(FhirContext theContext, IParserErrorHandler theParserErrorHandler) { 074 myContext = theContext; 075 myErrorHandler = theParserErrorHandler; 076 } 077 078 List<ElementsPath> getDontEncodeElements() { 079 return myDontEncodeElements; 080 } 081 082 @Override 083 public IParser setDontEncodeElements(Set<String> theDontEncodeElements) { 084 if (theDontEncodeElements == null || theDontEncodeElements.isEmpty()) { 085 myDontEncodeElements = null; 086 } else { 087 myDontEncodeElements = theDontEncodeElements 088 .stream() 089 .map(ElementsPath::new) 090 .collect(Collectors.toList()); 091 } 092 return this; 093 } 094 095 List<ElementsPath> getEncodeElements() { 096 return myEncodeElements; 097 } 098 099 @Override 100 public IParser setEncodeElements(Set<String> theEncodeElements) { 101 102 if (theEncodeElements == null || theEncodeElements.isEmpty()) { 103 myEncodeElements = null; 104 myEncodeElementsAppliesToResourceTypes = null; 105 } else { 106 myEncodeElements = theEncodeElements 107 .stream() 108 .map(ElementsPath::new) 109 .collect(Collectors.toList()); 110 111 myEncodeElementsAppliesToResourceTypes = new HashSet<>(); 112 for (String next : myEncodeElements.stream().map(t -> t.getPath().get(0).getName()).collect(Collectors.toList())) { 113 if (next.startsWith("*")) { 114 myEncodeElementsAppliesToResourceTypes = null; 115 break; 116 } 117 int dotIdx = next.indexOf('.'); 118 if (dotIdx == -1) { 119 myEncodeElementsAppliesToResourceTypes.add(next); 120 } else { 121 myEncodeElementsAppliesToResourceTypes.add(next.substring(0, dotIdx)); 122 } 123 } 124 125 } 126 127 return this; 128 } 129 130 protected Iterable<CompositeChildElement> compositeChildIterator(IBase theCompositeElement, final boolean theContainedResource, final CompositeChildElement theParent, EncodeContext theEncodeContext) { 131 132 BaseRuntimeElementCompositeDefinition<?> elementDef = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theCompositeElement.getClass()); 133 final List<BaseRuntimeChildDefinition> children = elementDef.getChildrenAndExtension(); 134 135 return new Iterable<BaseParser.CompositeChildElement>() { 136 137 @Override 138 public Iterator<CompositeChildElement> iterator() { 139 140 return new Iterator<CompositeChildElement>() { 141 private Iterator<? extends BaseRuntimeChildDefinition> myChildrenIter; 142 private Boolean myHasNext = null; 143 private CompositeChildElement myNext; 144 145 /** 146 * Constructor 147 */ { 148 myChildrenIter = children.iterator(); 149 } 150 151 @Override 152 public boolean hasNext() { 153 if (myHasNext != null) { 154 return myHasNext; 155 } 156 157 myNext = null; 158 do { 159 if (myChildrenIter.hasNext() == false) { 160 myHasNext = Boolean.FALSE; 161 return false; 162 } 163 164 myNext = new CompositeChildElement(theParent, myChildrenIter.next(), theEncodeContext); 165 166 /* 167 * There are lots of reasons we might skip encoding a particular child 168 */ 169 if (myNext.getDef().getElementName().equals("id")) { 170 myNext = null; 171 } else if (!myNext.shouldBeEncoded(theContainedResource)) { 172 myNext = null; 173 } else if (myNext.getDef() instanceof RuntimeChildNarrativeDefinition) { 174 if (isSuppressNarratives() || isSummaryMode()) { 175 myNext = null; 176 } else if (theContainedResource) { 177 myNext = null; 178 } 179 } else if (myNext.getDef() instanceof RuntimeChildContainedResources) { 180 if (theContainedResource) { 181 myNext = null; 182 } 183 } 184 } while (myNext == null); 185 186 myHasNext = true; 187 return true; 188 } 189 190 @Override 191 public CompositeChildElement next() { 192 if (myHasNext == null) { 193 if (!hasNext()) { 194 throw new IllegalStateException(); 195 } 196 } 197 CompositeChildElement retVal = myNext; 198 myNext = null; 199 myHasNext = null; 200 return retVal; 201 } 202 203 @Override 204 public void remove() { 205 throw new UnsupportedOperationException(); 206 } 207 }; 208 } 209 }; 210 } 211 212 private void containResourcesForEncoding(ContainedResources theContained, IBaseResource theResource, IBaseResource theTarget) { 213 214 if (theTarget instanceof IResource) { 215 List<? extends IResource> containedResources = ((IResource) theTarget).getContained().getContainedResources(); 216 for (IResource next : containedResources) { 217 String nextId = next.getId().getValue(); 218 if (StringUtils.isNotBlank(nextId)) { 219 if (!nextId.startsWith("#")) { 220 nextId = '#' + nextId; 221 } 222 theContained.getExistingIdToContainedResource().put(nextId, next); 223 } 224 } 225 } else if (theTarget instanceof IDomainResource) { 226 List<? extends IAnyResource> containedResources = ((IDomainResource) theTarget).getContained(); 227 for (IAnyResource next : containedResources) { 228 String nextId = next.getIdElement().getValue(); 229 if (StringUtils.isNotBlank(nextId)) { 230 if (!nextId.startsWith("#")) { 231 nextId = '#' + nextId; 232 } 233 theContained.getExistingIdToContainedResource().put(nextId, next); 234 } 235 } 236 } 237 238 List<IBaseReference> allReferences = myContext.newTerser().getAllPopulatedChildElementsOfType(theResource, IBaseReference.class); 239 for (IBaseReference next : allReferences) { 240 IBaseResource resource = next.getResource(); 241 if (resource == null && next.getReferenceElement().isLocal()) { 242 if (theContained.hasExistingIdToContainedResource()) { 243 IBaseResource potentialTarget = theContained.getExistingIdToContainedResource().remove(next.getReferenceElement().getValue()); 244 if (potentialTarget != null) { 245 theContained.addContained(next.getReferenceElement(), potentialTarget); 246 containResourcesForEncoding(theContained, potentialTarget, theTarget); 247 } 248 } 249 } 250 } 251 252 for (IBaseReference next : allReferences) { 253 IBaseResource resource = next.getResource(); 254 if (resource != null) { 255 if (resource.getIdElement().isEmpty() || resource.getIdElement().isLocal()) { 256 if (theContained.getResourceId(resource) != null) { 257 // Prevent infinite recursion if there are circular loops in the contained resources 258 continue; 259 } 260 theContained.addContained(resource); 261 if (resource.getIdElement().isLocal() && theContained.hasExistingIdToContainedResource()) { 262 theContained.getExistingIdToContainedResource().remove(resource.getIdElement().getValue()); 263 } 264 } else { 265 continue; 266 } 267 268 containResourcesForEncoding(theContained, resource, theTarget); 269 } 270 271 } 272 273 } 274 275 protected void containResourcesForEncoding(IBaseResource theResource) { 276 ContainedResources contained = new ContainedResources(); 277 containResourcesForEncoding(contained, theResource, theResource); 278 contained.assignIdsToContainedResources(); 279 myContainedResources = contained; 280 281 } 282 283 private String determineReferenceText(IBaseReference theRef, CompositeChildElement theCompositeChildElement) { 284 IIdType ref = theRef.getReferenceElement(); 285 if (isBlank(ref.getIdPart())) { 286 String reference = ref.getValue(); 287 if (theRef.getResource() != null) { 288 IIdType containedId = getContainedResources().getResourceId(theRef.getResource()); 289 if (containedId != null && !containedId.isEmpty()) { 290 if (containedId.isLocal()) { 291 reference = containedId.getValue(); 292 } else { 293 reference = "#" + containedId.getValue(); 294 } 295 } else { 296 IIdType refId = theRef.getResource().getIdElement(); 297 if (refId != null) { 298 if (refId.hasIdPart()) { 299 if (refId.getValue().startsWith("urn:")) { 300 reference = refId.getValue(); 301 } else { 302 if (!refId.hasResourceType()) { 303 refId = refId.withResourceType(myContext.getResourceDefinition(theRef.getResource()).getName()); 304 } 305 if (isStripVersionsFromReferences(theCompositeChildElement)) { 306 reference = refId.toVersionless().getValue(); 307 } else { 308 reference = refId.getValue(); 309 } 310 } 311 } 312 } 313 } 314 } 315 return reference; 316 } 317 if (!ref.hasResourceType() && !ref.isLocal() && theRef.getResource() != null) { 318 ref = ref.withResourceType(myContext.getResourceDefinition(theRef.getResource()).getName()); 319 } 320 if (isNotBlank(myServerBaseUrl) && StringUtils.equals(myServerBaseUrl, ref.getBaseUrl())) { 321 if (isStripVersionsFromReferences(theCompositeChildElement)) { 322 return ref.toUnqualifiedVersionless().getValue(); 323 } 324 return ref.toUnqualified().getValue(); 325 } 326 if (isStripVersionsFromReferences(theCompositeChildElement)) { 327 return ref.toVersionless().getValue(); 328 } 329 return ref.getValue(); 330 } 331 332 protected abstract void doEncodeResourceToWriter(IBaseResource theResource, Writer theWriter, EncodeContext theEncodeContext) throws IOException, DataFormatException; 333 334 protected abstract <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader) throws DataFormatException; 335 336 @Override 337 public String encodeResourceToString(IBaseResource theResource) throws DataFormatException { 338 Writer stringWriter = new StringWriter(); 339 try { 340 encodeResourceToWriter(theResource, stringWriter); 341 } catch (IOException e) { 342 throw new Error("Encountered IOException during write to string - This should not happen!"); 343 } 344 return stringWriter.toString(); 345 } 346 347 @Override 348 public final void encodeResourceToWriter(IBaseResource theResource, Writer theWriter) throws IOException, DataFormatException { 349 EncodeContext encodeContext = new EncodeContext(); 350 351 encodeResourceToWriter(theResource, theWriter, encodeContext); 352 } 353 354 protected void encodeResourceToWriter(IBaseResource theResource, Writer theWriter, EncodeContext theEncodeContext) throws IOException { 355 Validate.notNull(theResource, "theResource can not be null"); 356 Validate.notNull(theWriter, "theWriter can not be null"); 357 Validate.notNull(theEncodeContext, "theEncodeContext can not be null"); 358 359 if (theResource.getStructureFhirVersionEnum() != myContext.getVersion().getVersion()) { 360 throw new IllegalArgumentException( 361 "This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum()); 362 } 363 364 String resourceName = myContext.getResourceDefinition(theResource).getName(); 365 theEncodeContext.pushPath(resourceName, true); 366 367 doEncodeResourceToWriter(theResource, theWriter, theEncodeContext); 368 369 theEncodeContext.popPath(); 370 } 371 372 private void filterCodingsWithNoCodeOrSystem(List<? extends IBaseCoding> tagList) { 373 for (int i = 0; i < tagList.size(); i++) { 374 if (isBlank(tagList.get(i).getCode()) && isBlank(tagList.get(i).getSystem())) { 375 tagList.remove(i); 376 i--; 377 } 378 } 379 } 380 381 protected IIdType fixContainedResourceId(String theValue) { 382 IIdType retVal = (IIdType) myContext.getElementDefinition("id").newInstance(); 383 if (StringUtils.isNotBlank(theValue) && theValue.charAt(0) == '#') { 384 retVal.setValue(theValue.substring(1)); 385 } else { 386 retVal.setValue(theValue); 387 } 388 return retVal; 389 } 390 391 @SuppressWarnings("unchecked") 392 ChildNameAndDef getChildNameAndDef(BaseRuntimeChildDefinition theChild, IBase theValue) { 393 Class<? extends IBase> type = theValue.getClass(); 394 String childName = theChild.getChildNameByDatatype(type); 395 BaseRuntimeElementDefinition<?> childDef = theChild.getChildElementDefinitionByDatatype(type); 396 if (childDef == null) { 397 // if (theValue instanceof IBaseExtension) { 398 // return null; 399 // } 400 401 /* 402 * For RI structures Enumeration class, this replaces the child def 403 * with the "code" one. This is messy, and presumably there is a better 404 * way.. 405 */ 406 BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(type); 407 if (elementDef.getName().equals("code")) { 408 Class<? extends IBase> type2 = myContext.getElementDefinition("code").getImplementingClass(); 409 childDef = theChild.getChildElementDefinitionByDatatype(type2); 410 childName = theChild.getChildNameByDatatype(type2); 411 } 412 413 // See possibly the user has extended a built-in type without 414 // declaring it anywhere, as in XmlParserDstu3Test#testEncodeUndeclaredBlock 415 if (childDef == null) { 416 Class<?> nextSuperType = theValue.getClass(); 417 while (IBase.class.isAssignableFrom(nextSuperType) && childDef == null) { 418 if (Modifier.isAbstract(nextSuperType.getModifiers()) == false) { 419 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition((Class<? extends IBase>) nextSuperType); 420 Class<?> nextChildType = def.getImplementingClass(); 421 childDef = theChild.getChildElementDefinitionByDatatype((Class<? extends IBase>) nextChildType); 422 childName = theChild.getChildNameByDatatype((Class<? extends IBase>) nextChildType); 423 } 424 nextSuperType = nextSuperType.getSuperclass(); 425 } 426 } 427 428 if (childDef == null) { 429 throwExceptionForUnknownChildType(theChild, type); 430 } 431 } 432 433 return new ChildNameAndDef(childName, childDef); 434 } 435 436 protected String getCompositeElementId(IBase theElement) { 437 String elementId = null; 438 if (!(theElement instanceof IBaseResource)) { 439 if (theElement instanceof IBaseElement) { 440 elementId = ((IBaseElement) theElement).getId(); 441 } else if (theElement instanceof IIdentifiableElement) { 442 elementId = ((IIdentifiableElement) theElement).getElementSpecificId(); 443 } 444 } 445 return elementId; 446 } 447 448 ContainedResources getContainedResources() { 449 return myContainedResources; 450 } 451 452 @Override 453 public Set<String> getDontStripVersionsFromReferencesAtPaths() { 454 return myDontStripVersionsFromReferencesAtPaths; 455 } 456 457 @Override 458 public IIdType getEncodeForceResourceId() { 459 return myEncodeForceResourceId; 460 } 461 462 @Override 463 public BaseParser setEncodeForceResourceId(IIdType theEncodeForceResourceId) { 464 myEncodeForceResourceId = theEncodeForceResourceId; 465 return this; 466 } 467 468 protected IParserErrorHandler getErrorHandler() { 469 return myErrorHandler; 470 } 471 472 protected List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> getExtensionMetadataKeys(IResource resource) { 473 List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> extensionMetadataKeys = new ArrayList<>(); 474 for (Map.Entry<ResourceMetadataKeyEnum<?>, Object> entry : resource.getResourceMetadata().entrySet()) { 475 if (entry.getKey() instanceof ResourceMetadataKeyEnum.ExtensionResourceMetadataKey) { 476 extensionMetadataKeys.add(entry); 477 } 478 } 479 480 return extensionMetadataKeys; 481 } 482 483 protected String getExtensionUrl(final String extensionUrl) { 484 String url = extensionUrl; 485 if (StringUtils.isNotBlank(extensionUrl) && StringUtils.isNotBlank(myServerBaseUrl)) { 486 url = !UrlUtil.isValid(extensionUrl) && extensionUrl.startsWith("/") ? myServerBaseUrl + extensionUrl : extensionUrl; 487 } 488 return url; 489 } 490 491 protected TagList getMetaTagsForEncoding(IResource theIResource, EncodeContext theEncodeContext) { 492 TagList tags = ResourceMetadataKeyEnum.TAG_LIST.get(theIResource); 493 if (shouldAddSubsettedTag(theEncodeContext)) { 494 tags = new TagList(tags); 495 tags.add(new Tag(getSubsettedCodeSystem(), Constants.TAG_SUBSETTED_CODE, subsetDescription())); 496 } 497 498 return tags; 499 } 500 501 @Override 502 public Boolean getOverrideResourceIdWithBundleEntryFullUrl() { 503 return myOverrideResourceIdWithBundleEntryFullUrl; 504 } 505 506 @Override 507 public List<Class<? extends IBaseResource>> getPreferTypes() { 508 return myPreferTypes; 509 } 510 511 @Override 512 public void setPreferTypes(List<Class<? extends IBaseResource>> thePreferTypes) { 513 if (thePreferTypes != null) { 514 ArrayList<Class<? extends IBaseResource>> types = new ArrayList<>(); 515 for (Class<? extends IBaseResource> next : thePreferTypes) { 516 if (Modifier.isAbstract(next.getModifiers()) == false) { 517 types.add(next); 518 } 519 } 520 myPreferTypes = Collections.unmodifiableList(types); 521 } else { 522 myPreferTypes = thePreferTypes; 523 } 524 } 525 526 @SuppressWarnings("deprecation") 527 protected <T extends IPrimitiveType<String>> List<T> getProfileTagsForEncoding(IBaseResource theResource, List<T> theProfiles) { 528 switch (myContext.getAddProfileTagWhenEncoding()) { 529 case NEVER: 530 return theProfiles; 531 case ONLY_FOR_CUSTOM: 532 RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource); 533 if (resDef.isStandardType()) { 534 return theProfiles; 535 } 536 break; 537 case ALWAYS: 538 break; 539 } 540 541 RuntimeResourceDefinition nextDef = myContext.getResourceDefinition(theResource); 542 String profile = nextDef.getResourceProfile(myServerBaseUrl); 543 if (isNotBlank(profile)) { 544 for (T next : theProfiles) { 545 if (profile.equals(next.getValue())) { 546 return theProfiles; 547 } 548 } 549 550 List<T> newList = new ArrayList<>(theProfiles); 551 552 BaseRuntimeElementDefinition<?> idElement = myContext.getElementDefinition("id"); 553 @SuppressWarnings("unchecked") 554 T newId = (T) idElement.newInstance(); 555 newId.setValue(profile); 556 557 newList.add(newId); 558 return newList; 559 } 560 561 return theProfiles; 562 } 563 564 protected String getServerBaseUrl() { 565 return myServerBaseUrl; 566 } 567 568 @Override 569 public Boolean getStripVersionsFromReferences() { 570 return myStripVersionsFromReferences; 571 } 572 573 /** 574 * If set to <code>true</code> (default is <code>false</code>), narratives will not be included in the encoded 575 * values. 576 * 577 * @deprecated Use {@link #isSuppressNarratives()} 578 */ 579 @Deprecated 580 public boolean getSuppressNarratives() { 581 return mySuppressNarratives; 582 } 583 584 protected boolean isChildContained(BaseRuntimeElementDefinition<?> childDef, boolean theIncludedResource) { 585 return (childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES || childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) && getContainedResources().isEmpty() == false 586 && theIncludedResource == false; 587 } 588 589 @Override 590 public boolean isEncodeElementsAppliesToChildResourcesOnly() { 591 return myEncodeElementsAppliesToChildResourcesOnly; 592 } 593 594 @Override 595 public void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly) { 596 myEncodeElementsAppliesToChildResourcesOnly = theEncodeElementsAppliesToChildResourcesOnly; 597 } 598 599 @Override 600 public boolean isOmitResourceId() { 601 return myOmitResourceId; 602 } 603 604 private boolean isOverrideResourceIdWithBundleEntryFullUrl() { 605 Boolean overrideResourceIdWithBundleEntryFullUrl = myOverrideResourceIdWithBundleEntryFullUrl; 606 if (overrideResourceIdWithBundleEntryFullUrl != null) { 607 return overrideResourceIdWithBundleEntryFullUrl; 608 } 609 610 return myContext.getParserOptions().isOverrideResourceIdWithBundleEntryFullUrl(); 611 } 612 613 private boolean isStripVersionsFromReferences(CompositeChildElement theCompositeChildElement) { 614 Boolean stripVersionsFromReferences = myStripVersionsFromReferences; 615 if (stripVersionsFromReferences != null) { 616 return stripVersionsFromReferences; 617 } 618 619 if (myContext.getParserOptions().isStripVersionsFromReferences() == false) { 620 return false; 621 } 622 623 Set<String> dontStripVersionsFromReferencesAtPaths = myDontStripVersionsFromReferencesAtPaths; 624 if (dontStripVersionsFromReferencesAtPaths != null) { 625 if (dontStripVersionsFromReferencesAtPaths.isEmpty() == false && theCompositeChildElement.anyPathMatches(dontStripVersionsFromReferencesAtPaths)) { 626 return false; 627 } 628 } 629 630 dontStripVersionsFromReferencesAtPaths = myContext.getParserOptions().getDontStripVersionsFromReferencesAtPaths(); 631 return dontStripVersionsFromReferencesAtPaths.isEmpty() != false || !theCompositeChildElement.anyPathMatches(dontStripVersionsFromReferencesAtPaths); 632 } 633 634 @Override 635 public boolean isSummaryMode() { 636 return mySummaryMode; 637 } 638 639 /** 640 * If set to <code>true</code> (default is <code>false</code>), narratives will not be included in the encoded 641 * values. 642 * 643 * @since 1.2 644 */ 645 public boolean isSuppressNarratives() { 646 return mySuppressNarratives; 647 } 648 649 @Override 650 public IBaseResource parseResource(InputStream theInputStream) throws DataFormatException { 651 return parseResource(new InputStreamReader(theInputStream, Charsets.UTF_8)); 652 } 653 654 @Override 655 public <T extends IBaseResource> T parseResource(Class<T> theResourceType, InputStream theInputStream) throws DataFormatException { 656 return parseResource(theResourceType, new InputStreamReader(theInputStream, Constants.CHARSET_UTF8)); 657 } 658 659 @Override 660 public <T extends IBaseResource> T parseResource(Class<T> theResourceType, Reader theReader) throws DataFormatException { 661 662 /* 663 * We do this so that the context can verify that the structure is for 664 * the correct FHIR version 665 */ 666 if (theResourceType != null) { 667 myContext.getResourceDefinition(theResourceType); 668 } 669 670 // Actually do the parse 671 T retVal = doParseResource(theResourceType, theReader); 672 673 RuntimeResourceDefinition def = myContext.getResourceDefinition(retVal); 674 if ("Bundle".equals(def.getName())) { 675 676 BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); 677 BaseRuntimeElementCompositeDefinition<?> entryDef = (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry"); 678 List<IBase> entries = entryChild.getAccessor().getValues(retVal); 679 if (entries != null) { 680 for (IBase nextEntry : entries) { 681 682 /** 683 * If Bundle.entry.fullUrl is populated, set the resource ID to that 684 */ 685 // TODO: should emit a warning and maybe notify the error handler if the resource ID doesn't match the 686 // fullUrl idPart 687 BaseRuntimeChildDefinition fullUrlChild = entryDef.getChildByName("fullUrl"); 688 if (fullUrlChild == null) { 689 continue; // TODO: remove this once the data model in tinder plugin catches up to 1.2 690 } 691 if (isOverrideResourceIdWithBundleEntryFullUrl()) { 692 List<IBase> fullUrl = fullUrlChild.getAccessor().getValues(nextEntry); 693 if (fullUrl != null && !fullUrl.isEmpty()) { 694 IPrimitiveType<?> value = (IPrimitiveType<?>) fullUrl.get(0); 695 if (value.isEmpty() == false) { 696 List<IBase> entryResources = entryDef.getChildByName("resource").getAccessor().getValues(nextEntry); 697 if (entryResources != null && entryResources.size() > 0) { 698 IBaseResource res = (IBaseResource) entryResources.get(0); 699 String versionId = res.getIdElement().getVersionIdPart(); 700 res.setId(value.getValueAsString()); 701 if (isNotBlank(versionId) && res.getIdElement().hasVersionIdPart() == false) { 702 res.setId(res.getIdElement().withVersion(versionId)); 703 } 704 } 705 } 706 } 707 } 708 } 709 } 710 711 } 712 713 return retVal; 714 } 715 716 @SuppressWarnings("cast") 717 @Override 718 public <T extends IBaseResource> T parseResource(Class<T> theResourceType, String theMessageString) { 719 StringReader reader = new StringReader(theMessageString); 720 return parseResource(theResourceType, reader); 721 } 722 723 @Override 724 public IBaseResource parseResource(Reader theReader) throws ConfigurationException, DataFormatException { 725 return parseResource(null, theReader); 726 } 727 728 @Override 729 public IBaseResource parseResource(String theMessageString) throws ConfigurationException, DataFormatException { 730 return parseResource(null, theMessageString); 731 } 732 733 protected List<? extends IBase> preProcessValues(BaseRuntimeChildDefinition theMetaChildUncast, IBaseResource theResource, List<? extends IBase> theValues, 734 CompositeChildElement theCompositeChildElement, EncodeContext theEncodeContext) { 735 if (myContext.getVersion().getVersion().isRi()) { 736 737 /* 738 * If we're encoding the meta tag, we do some massaging of the meta values before 739 * encoding. But if there is no meta element at all, we create one since we're possibly going to be 740 * adding things to it 741 */ 742 if (theValues.isEmpty() && theMetaChildUncast.getElementName().equals("meta")) { 743 BaseRuntimeElementDefinition<?> metaChild = theMetaChildUncast.getChildByName("meta"); 744 if (IBaseMetaType.class.isAssignableFrom(metaChild.getImplementingClass())) { 745 IBaseMetaType newType = (IBaseMetaType) metaChild.newInstance(); 746 theValues = Collections.singletonList(newType); 747 } 748 } 749 750 if (theValues.size() == 1 && theValues.get(0) instanceof IBaseMetaType) { 751 752 IBaseMetaType metaValue = (IBaseMetaType) theValues.get(0); 753 try { 754 metaValue = (IBaseMetaType) metaValue.getClass().getMethod("copy").invoke(metaValue); 755 } catch (Exception e) { 756 throw new InternalErrorException("Failed to duplicate meta", e); 757 } 758 759 if (isBlank(metaValue.getVersionId())) { 760 if (theResource.getIdElement().hasVersionIdPart()) { 761 metaValue.setVersionId(theResource.getIdElement().getVersionIdPart()); 762 } 763 } 764 765 filterCodingsWithNoCodeOrSystem(metaValue.getTag()); 766 filterCodingsWithNoCodeOrSystem(metaValue.getSecurity()); 767 768 List<? extends IPrimitiveType<String>> newProfileList = getProfileTagsForEncoding(theResource, metaValue.getProfile()); 769 List<? extends IPrimitiveType<String>> oldProfileList = metaValue.getProfile(); 770 if (oldProfileList != newProfileList) { 771 oldProfileList.clear(); 772 for (IPrimitiveType<String> next : newProfileList) { 773 if (isNotBlank(next.getValue())) { 774 metaValue.addProfile(next.getValue()); 775 } 776 } 777 } 778 779 if (shouldAddSubsettedTag(theEncodeContext)) { 780 IBaseCoding coding = metaValue.addTag(); 781 coding.setCode(Constants.TAG_SUBSETTED_CODE); 782 coding.setSystem(getSubsettedCodeSystem()); 783 coding.setDisplay(subsetDescription()); 784 } 785 786 return Collections.singletonList(metaValue); 787 } 788 } 789 790 @SuppressWarnings("unchecked") 791 List<IBase> retVal = (List<IBase>) theValues; 792 793 for (int i = 0; i < retVal.size(); i++) { 794 IBase next = retVal.get(i); 795 796 /* 797 * If we have automatically contained any resources via 798 * their references, this ensures that we output the new 799 * local reference 800 */ 801 if (next instanceof IBaseReference) { 802 IBaseReference nextRef = (IBaseReference) next; 803 String refText = determineReferenceText(nextRef, theCompositeChildElement); 804 if (!StringUtils.equals(refText, nextRef.getReferenceElement().getValue())) { 805 806 if (retVal == theValues) { 807 retVal = new ArrayList<>(theValues); 808 } 809 IBaseReference newRef = (IBaseReference) myContext.getElementDefinition(nextRef.getClass()).newInstance(); 810 myContext.newTerser().cloneInto(nextRef, newRef, true); 811 newRef.setReference(refText); 812 retVal.set(i, newRef); 813 814 } 815 } 816 } 817 818 return retVal; 819 } 820 821 private String getSubsettedCodeSystem() { 822 if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) { 823 return Constants.TAG_SUBSETTED_SYSTEM_R4; 824 } else { 825 return Constants.TAG_SUBSETTED_SYSTEM_DSTU3; 826 } 827 } 828 829 @Override 830 public IParser setDontStripVersionsFromReferencesAtPaths(String... thePaths) { 831 if (thePaths == null) { 832 setDontStripVersionsFromReferencesAtPaths((List<String>) null); 833 } else { 834 setDontStripVersionsFromReferencesAtPaths(Arrays.asList(thePaths)); 835 } 836 return this; 837 } 838 839 @SuppressWarnings("unchecked") 840 @Override 841 public IParser setDontStripVersionsFromReferencesAtPaths(Collection<String> thePaths) { 842 if (thePaths == null) { 843 myDontStripVersionsFromReferencesAtPaths = Collections.emptySet(); 844 } else if (thePaths instanceof HashSet) { 845 myDontStripVersionsFromReferencesAtPaths = (Set<String>) ((HashSet<String>) thePaths).clone(); 846 } else { 847 myDontStripVersionsFromReferencesAtPaths = new HashSet<>(thePaths); 848 } 849 return this; 850 } 851 852 @Override 853 public IParser setOmitResourceId(boolean theOmitResourceId) { 854 myOmitResourceId = theOmitResourceId; 855 return this; 856 } 857 858 @Override 859 public IParser setOverrideResourceIdWithBundleEntryFullUrl(Boolean theOverrideResourceIdWithBundleEntryFullUrl) { 860 myOverrideResourceIdWithBundleEntryFullUrl = theOverrideResourceIdWithBundleEntryFullUrl; 861 return this; 862 } 863 864 @Override 865 public IParser setParserErrorHandler(IParserErrorHandler theErrorHandler) { 866 Validate.notNull(theErrorHandler, "theErrorHandler must not be null"); 867 myErrorHandler = theErrorHandler; 868 return this; 869 } 870 871 @Override 872 public IParser setServerBaseUrl(String theUrl) { 873 myServerBaseUrl = isNotBlank(theUrl) ? theUrl : null; 874 return this; 875 } 876 877 @Override 878 public IParser setStripVersionsFromReferences(Boolean theStripVersionsFromReferences) { 879 myStripVersionsFromReferences = theStripVersionsFromReferences; 880 return this; 881 } 882 883 @Override 884 public IParser setSummaryMode(boolean theSummaryMode) { 885 mySummaryMode = theSummaryMode; 886 return this; 887 } 888 889 @Override 890 public IParser setSuppressNarratives(boolean theSuppressNarratives) { 891 mySuppressNarratives = theSuppressNarratives; 892 return this; 893 } 894 895 protected boolean shouldAddSubsettedTag(EncodeContext theEncodeContext) { 896 if (isSummaryMode()) { 897 return true; 898 } 899 if (isSuppressNarratives()) { 900 return true; 901 } 902 if (myEncodeElements != null) { 903 if (isEncodeElementsAppliesToChildResourcesOnly() && theEncodeContext.getResourcePath().size() < 2) { 904 return false; 905 } 906 907 String currentResourceName = theEncodeContext.getResourcePath().get(theEncodeContext.getResourcePath().size() - 1).getName(); 908 return myEncodeElementsAppliesToResourceTypes == null || myEncodeElementsAppliesToResourceTypes.contains(currentResourceName); 909 } 910 911 return false; 912 } 913 914 protected boolean shouldEncodeResourceId(IBaseResource theResource, EncodeContext theEncodeContext) { 915 boolean retVal = true; 916 if (isOmitResourceId()) { 917 retVal = false; 918 } else { 919 if (myDontEncodeElements != null) { 920 String resourceName = myContext.getResourceDefinition(theResource).getName(); 921 if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath(resourceName + ".id"))) { 922 retVal = false; 923 } else if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath("*.id"))) { 924 retVal = false; 925 } else if (theEncodeContext.getResourcePath().size() == 1 && myDontEncodeElements.stream().anyMatch(t -> t.equalsPath("id"))) { 926 retVal = false; 927 } 928 } 929 } 930 return retVal; 931 } 932 933 /** 934 * Used for DSTU2 only 935 */ 936 protected boolean shouldEncodeResourceMeta(IResource theResource) { 937 return shouldEncodePath(theResource, "meta"); 938 } 939 940 /** 941 * Used for DSTU2 only 942 */ 943 protected boolean shouldEncodePath(IResource theResource, String thePath) { 944 if (myDontEncodeElements != null) { 945 String resourceName = myContext.getResourceDefinition(theResource).getName(); 946 if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath(resourceName + "." + thePath))) { 947 return false; 948 } else return myDontEncodeElements.stream().noneMatch(t -> t.equalsPath("*." + thePath)); 949 } 950 return true; 951 } 952 953 private String subsetDescription() { 954 return "Resource encoded in summary mode"; 955 } 956 957 protected void throwExceptionForUnknownChildType(BaseRuntimeChildDefinition nextChild, Class<? extends IBase> theType) { 958 if (nextChild instanceof BaseRuntimeDeclaredChildDefinition) { 959 StringBuilder b = new StringBuilder(); 960 b.append(nextChild.getElementName()); 961 b.append(" has type "); 962 b.append(theType.getName()); 963 b.append(" but this is not a valid type for this element"); 964 if (nextChild instanceof RuntimeChildChoiceDefinition) { 965 RuntimeChildChoiceDefinition choice = (RuntimeChildChoiceDefinition) nextChild; 966 b.append(" - Expected one of: " + choice.getValidChildTypes()); 967 } 968 throw new DataFormatException(b.toString()); 969 } 970 throw new DataFormatException(nextChild + " has no child of type " + theType); 971 } 972 973 protected boolean shouldEncodeResource(String theName) { 974 if (myDontEncodeElements != null) { 975 for (ElementsPath next : myDontEncodeElements) { 976 if (next.equalsPath(theName)) { 977 return false; 978 } 979 } 980 } 981 return true; 982 } 983 984 class ChildNameAndDef { 985 986 private final BaseRuntimeElementDefinition<?> myChildDef; 987 private final String myChildName; 988 989 public ChildNameAndDef(String theChildName, BaseRuntimeElementDefinition<?> theChildDef) { 990 myChildName = theChildName; 991 myChildDef = theChildDef; 992 } 993 994 public BaseRuntimeElementDefinition<?> getChildDef() { 995 return myChildDef; 996 } 997 998 public String getChildName() { 999 return myChildName; 1000 } 1001 1002 } 1003 1004 protected class CompositeChildElement { 1005 private final BaseRuntimeChildDefinition myDef; 1006 private final CompositeChildElement myParent; 1007 private final RuntimeResourceDefinition myResDef; 1008 private final EncodeContext myEncodeContext; 1009 1010 public CompositeChildElement(CompositeChildElement theParent, @Nullable BaseRuntimeChildDefinition theDef, EncodeContext theEncodeContext) { 1011 myDef = theDef; 1012 myParent = theParent; 1013 myResDef = null; 1014 myEncodeContext = theEncodeContext; 1015 1016 if (ourLog.isTraceEnabled()) { 1017 if (theParent != null) { 1018 StringBuilder path = theParent.buildPath(); 1019 if (path != null) { 1020 path.append('.'); 1021 if (myDef != null) { 1022 path.append(myDef.getElementName()); 1023 } 1024 ourLog.trace(" * Next path: {}", path.toString()); 1025 } 1026 } 1027 } 1028 1029 } 1030 1031 public CompositeChildElement(RuntimeResourceDefinition theResDef, EncodeContext theEncodeContext) { 1032 myResDef = theResDef; 1033 myDef = null; 1034 myParent = null; 1035 myEncodeContext = theEncodeContext; 1036 } 1037 1038 private void addParent(CompositeChildElement theParent, StringBuilder theB) { 1039 if (theParent != null) { 1040 if (theParent.myResDef != null) { 1041 theB.append(theParent.myResDef.getName()); 1042 return; 1043 } 1044 1045 if (theParent.myParent != null) { 1046 addParent(theParent.myParent, theB); 1047 } 1048 1049 if (theParent.myDef != null) { 1050 if (theB.length() > 0) { 1051 theB.append('.'); 1052 } 1053 theB.append(theParent.myDef.getElementName()); 1054 } 1055 } 1056 } 1057 1058 public boolean anyPathMatches(Set<String> thePaths) { 1059 StringBuilder b = new StringBuilder(); 1060 addParent(this, b); 1061 1062 String path = b.toString(); 1063 return thePaths.contains(path); 1064 } 1065 1066 private StringBuilder buildPath() { 1067 if (myResDef != null) { 1068 StringBuilder b = new StringBuilder(); 1069 b.append(myResDef.getName()); 1070 return b; 1071 } else if (myParent != null) { 1072 StringBuilder b = myParent.buildPath(); 1073 if (b != null && myDef != null) { 1074 b.append('.'); 1075 b.append(myDef.getElementName()); 1076 } 1077 return b; 1078 } else { 1079 return null; 1080 } 1081 } 1082 1083 private boolean checkIfParentShouldBeEncodedAndBuildPath() { 1084 List<ElementsPath> encodeElements = myEncodeElements; 1085 1086 String currentResourceName = myEncodeContext.getResourcePath().get(myEncodeContext.getResourcePath().size() - 1).getName(); 1087 if (myEncodeElementsAppliesToResourceTypes != null && !myEncodeElementsAppliesToResourceTypes.contains(currentResourceName)) { 1088 encodeElements = null; 1089 } 1090 1091 boolean retVal = checkIfPathMatchesForEncoding(encodeElements, true); 1092 1093 /* 1094 * We force the meta tag to be encoded even if it's not specified as an element in the 1095 * elements filter, specifically because we'll need it in order to automatically add 1096 * the SUBSETTED tag 1097 */ 1098 if (!retVal) { 1099 if ("meta".equals(myEncodeContext.getLeafResourcePathFirstField()) && shouldAddSubsettedTag(myEncodeContext)) { 1100 // The next element is a child of the <meta> element 1101 retVal = true; 1102 } else if ("meta".equals(myDef.getElementName()) && shouldAddSubsettedTag(myEncodeContext)) { 1103 // The next element is the <meta> element 1104 retVal = true; 1105 } 1106 } 1107 1108 return retVal; 1109 } 1110 1111 private boolean checkIfParentShouldNotBeEncodedAndBuildPath() { 1112 return checkIfPathMatchesForEncoding(myDontEncodeElements, false); 1113 } 1114 1115 private boolean checkIfPathMatchesForEncoding(List<ElementsPath> theElements, boolean theCheckingForEncodeElements) { 1116 1117 boolean retVal = false; 1118 if (myDef != null) { 1119 myEncodeContext.pushPath(myDef.getElementName(), false); 1120 } 1121 1122 if (theCheckingForEncodeElements && isEncodeElementsAppliesToChildResourcesOnly() && myEncodeContext.getResourcePath().size() < 2) { 1123 retVal = true; 1124 } else if (theElements == null) { 1125 retVal = true; 1126 } else { 1127 EncodeContextPath currentResourcePath = myEncodeContext.getCurrentResourcePath(); 1128 ourLog.trace("Current resource path: {}", currentResourcePath); 1129 for (ElementsPath next : theElements) { 1130 1131 if (next.startsWith(currentResourcePath)) { 1132 if (theCheckingForEncodeElements || next.getPath().size() == currentResourcePath.getPath().size()) { 1133 retVal = true; 1134 break; 1135 } 1136 } 1137 1138 if (next.getPath().get(next.getPath().size() - 1).getName().equals("(mandatory)")) { 1139 if (myDef.getMin() > 0) { 1140 retVal = true; 1141 break; 1142 } 1143 if (currentResourcePath.getPath().size() > next.getPath().size()) { 1144 retVal = true; 1145 break; 1146 } 1147 } 1148 1149 } 1150 } 1151 1152 if (myDef != null) { 1153 myEncodeContext.popPath(); 1154 } 1155 1156 return retVal; 1157 } 1158 1159 public BaseRuntimeChildDefinition getDef() { 1160 return myDef; 1161 } 1162 1163 public CompositeChildElement getParent() { 1164 return myParent; 1165 } 1166 1167 public boolean shouldBeEncoded(boolean theContainedResource) { 1168 boolean retVal = true; 1169 if (myEncodeElements != null) { 1170 retVal = checkIfParentShouldBeEncodedAndBuildPath(); 1171 } 1172 if (retVal && myDontEncodeElements != null) { 1173 retVal = !checkIfParentShouldNotBeEncodedAndBuildPath(); 1174 } 1175 if (theContainedResource) { 1176 retVal = !notEncodeForContainedResource.contains(myDef.getElementName()); 1177 } 1178 if (retVal && isSummaryMode() && (getDef() == null || !getDef().isSummary())) { 1179 String resourceName = myEncodeContext.getLeafResourceName(); 1180 // Technically the spec says we shouldn't include extensions in CapabilityStatement 1181 // but we will do so because there are people who depend on this behaviour, at least 1182 // as of 2019-07. See 1183 // https://github.com/smart-on-fhir/Swift-FHIR/issues/26 1184 // for example. 1185 if (("Conformance".equals(resourceName) || "CapabilityStatement".equals(resourceName)) && 1186 ("extension".equals(myDef.getElementName()) || "extension".equals(myEncodeContext.getLeafElementName()) 1187 )) { 1188 // skip 1189 } else { 1190 retVal = false; 1191 } 1192 } 1193 1194 return retVal; 1195 } 1196 } 1197 1198 protected class EncodeContextPath { 1199 private final List<EncodeContextPathElement> myPath; 1200 1201 public EncodeContextPath() { 1202 myPath = new ArrayList<>(10); 1203 } 1204 1205 public EncodeContextPath(List<EncodeContextPathElement> thePath) { 1206 myPath = thePath; 1207 } 1208 1209 @Override 1210 public String toString() { 1211 return myPath.stream().map(t -> t.toString()).collect(Collectors.joining(".")); 1212 } 1213 1214 protected List<EncodeContextPathElement> getPath() { 1215 return myPath; 1216 } 1217 1218 public EncodeContextPath getCurrentResourcePath() { 1219 EncodeContextPath retVal = null; 1220 for (int i = myPath.size() - 1; i >= 0; i--) { 1221 if (myPath.get(i).isResource()) { 1222 retVal = new EncodeContextPath(myPath.subList(i, myPath.size())); 1223 break; 1224 } 1225 } 1226 Validate.isTrue(retVal != null); 1227 return retVal; 1228 } 1229 } 1230 1231 protected class ElementsPath extends EncodeContextPath { 1232 1233 protected ElementsPath(String thePath) { 1234 StringTokenizer tok = new StringTokenizer(thePath, "."); 1235 boolean first = true; 1236 while (tok.hasMoreTokens()) { 1237 String next = tok.nextToken(); 1238 if (first && next.equals("*")) { 1239 getPath().add(new EncodeContextPathElement("*", true)); 1240 } else if (isNotBlank(next)) { 1241 getPath().add(new EncodeContextPathElement(next, Character.isUpperCase(next.charAt(0)))); 1242 } 1243 first = false; 1244 } 1245 } 1246 1247 public boolean startsWith(EncodeContextPath theCurrentResourcePath) { 1248 for (int i = 0; i < getPath().size(); i++) { 1249 if (theCurrentResourcePath.getPath().size() == i) { 1250 return true; 1251 } 1252 EncodeContextPathElement expected = getPath().get(i); 1253 EncodeContextPathElement actual = theCurrentResourcePath.getPath().get(i); 1254 if (!expected.matches(actual)) { 1255 return false; 1256 } 1257 } 1258 return true; 1259 } 1260 1261 public boolean equalsPath(String thePath) { 1262 ElementsPath parsedPath = new ElementsPath(thePath); 1263 return getPath().equals(parsedPath.getPath()); 1264 } 1265 } 1266 1267 1268 /** 1269 * EncodeContext is a shared state object that is passed around the 1270 * encode process 1271 */ 1272 protected class EncodeContext extends EncodeContextPath { 1273 private final ArrayList<EncodeContextPathElement> myResourcePath = new ArrayList<>(10); 1274 1275 protected ArrayList<EncodeContextPathElement> getResourcePath() { 1276 return myResourcePath; 1277 } 1278 1279 public String getLeafElementName() { 1280 return getPath().get(getPath().size() - 1).getName(); 1281 } 1282 1283 public String getLeafResourceName() { 1284 return myResourcePath.get(myResourcePath.size() - 1).getName(); 1285 } 1286 1287 public String getLeafResourcePathFirstField() { 1288 String retVal = null; 1289 for (int i = getPath().size() - 1; i >= 0; i--) { 1290 if (getPath().get(i).isResource()) { 1291 break; 1292 } else { 1293 retVal = getPath().get(i).getName(); 1294 } 1295 } 1296 return retVal; 1297 } 1298 1299 1300 /** 1301 * Add an element at the end of the path 1302 */ 1303 protected void pushPath(String thePathElement, boolean theResource) { 1304 assert isNotBlank(thePathElement); 1305 assert !thePathElement.contains("."); 1306 assert theResource ^ Character.isLowerCase(thePathElement.charAt(0)); 1307 1308 EncodeContextPathElement element = new EncodeContextPathElement(thePathElement, theResource); 1309 getPath().add(element); 1310 if (theResource) { 1311 myResourcePath.add(element); 1312 } 1313 } 1314 1315 /** 1316 * Remove the element at the end of the path 1317 */ 1318 public void popPath() { 1319 EncodeContextPathElement removed = getPath().remove(getPath().size() - 1); 1320 if (removed.isResource()) { 1321 myResourcePath.remove(myResourcePath.size() - 1); 1322 } 1323 } 1324 1325 1326 } 1327 1328 protected class EncodeContextPathElement { 1329 private final String myName; 1330 private final boolean myResource; 1331 1332 public EncodeContextPathElement(String theName, boolean theResource) { 1333 Validate.notBlank(theName); 1334 myName = theName; 1335 myResource = theResource; 1336 } 1337 1338 1339 public boolean matches(EncodeContextPathElement theOther) { 1340 if (myResource != theOther.isResource()) { 1341 return false; 1342 } 1343 String otherName = theOther.getName(); 1344 if (myName.equals(otherName)) { 1345 return true; 1346 } 1347 /* 1348 * This is here to handle situations where a path like 1349 * Observation.valueQuantity has been specified as an include/exclude path, 1350 * since we only know that path as 1351 * Observation.value 1352 * until we get to actually looking at the values there. 1353 */ 1354 if (myName.length() > otherName.length() && myName.startsWith(otherName)) { 1355 char ch = myName.charAt(otherName.length()); 1356 if (Character.isUpperCase(ch)) { 1357 return true; 1358 } 1359 } 1360 return myName.equals("*"); 1361 } 1362 1363 @Override 1364 public boolean equals(Object theO) { 1365 if (this == theO) { 1366 return true; 1367 } 1368 1369 if (theO == null || getClass() != theO.getClass()) { 1370 return false; 1371 } 1372 1373 EncodeContextPathElement that = (EncodeContextPathElement) theO; 1374 1375 return new EqualsBuilder() 1376 .append(myResource, that.myResource) 1377 .append(myName, that.myName) 1378 .isEquals(); 1379 } 1380 1381 @Override 1382 public int hashCode() { 1383 return new HashCodeBuilder(17, 37) 1384 .append(myName) 1385 .append(myResource) 1386 .toHashCode(); 1387 } 1388 1389 @Override 1390 public String toString() { 1391 if (myResource) { 1392 return myName + "(res)"; 1393 } 1394 return myName; 1395 } 1396 1397 public String getName() { 1398 return myName; 1399 } 1400 1401 public boolean isResource() { 1402 return myResource; 1403 } 1404 } 1405 1406 static class ContainedResources { 1407 private long myNextContainedId = 1; 1408 1409 private List<IBaseResource> myResourceList; 1410 private IdentityHashMap<IBaseResource, IIdType> myResourceToIdMap; 1411 private Map<String, IBaseResource> myExistingIdToContainedResourceMap; 1412 1413 public Map<String, IBaseResource> getExistingIdToContainedResource() { 1414 if (myExistingIdToContainedResourceMap == null) { 1415 myExistingIdToContainedResourceMap = new HashMap<>(); 1416 } 1417 return myExistingIdToContainedResourceMap; 1418 } 1419 1420 public void addContained(IBaseResource theResource) { 1421 if (getResourceToIdMap().containsKey(theResource)) { 1422 return; 1423 } 1424 1425 IIdType newId; 1426 if (theResource.getIdElement().isLocal()) { 1427 newId = theResource.getIdElement(); 1428 } else { 1429 newId = null; 1430 } 1431 1432 getResourceToIdMap().put(theResource, newId); 1433 getResourceList().add(theResource); 1434 } 1435 1436 public void addContained(IIdType theId, IBaseResource theResource) { 1437 if (!getResourceToIdMap().containsKey(theResource)) { 1438 getResourceToIdMap().put(theResource, theId); 1439 getResourceList().add(theResource); 1440 } 1441 } 1442 1443 public List<IBaseResource> getContainedResources() { 1444 if (getResourceToIdMap() == null) { 1445 return Collections.emptyList(); 1446 } 1447 return getResourceList(); 1448 } 1449 1450 public IIdType getResourceId(IBaseResource theNext) { 1451 if (getResourceToIdMap() == null) { 1452 return null; 1453 } 1454 return getResourceToIdMap().get(theNext); 1455 } 1456 1457 private List<IBaseResource> getResourceList() { 1458 if (myResourceList == null) { 1459 myResourceList = new ArrayList<>(); 1460 } 1461 return myResourceList; 1462 } 1463 1464 private IdentityHashMap<IBaseResource, IIdType> getResourceToIdMap() { 1465 if (myResourceToIdMap == null) { 1466 myResourceToIdMap = new IdentityHashMap<>(); 1467 } 1468 return myResourceToIdMap; 1469 } 1470 1471 public boolean isEmpty() { 1472 if (myResourceToIdMap == null) { 1473 return true; 1474 } 1475 return myResourceToIdMap.isEmpty(); 1476 } 1477 1478 public boolean hasExistingIdToContainedResource() { 1479 return myExistingIdToContainedResourceMap != null; 1480 } 1481 1482 public void assignIdsToContainedResources() { 1483 1484 if (getResourceList() != null) { 1485 1486 /* 1487 * The idea with the code block below: 1488 * 1489 * We want to preserve any IDs that were user-assigned, so that if it's really 1490 * important to someone that their contained resource have the ID of #FOO 1491 * or #1 we will keep that. 1492 * 1493 * For any contained resources where no ID was assigned by the user, we 1494 * want to manually create an ID but make sure we don't reuse an existing ID. 1495 */ 1496 1497 Set<String> ids = new HashSet<>(); 1498 1499 // Gather any user assigned IDs 1500 for (IBaseResource nextResource : getResourceList()) { 1501 if (getResourceToIdMap().get(nextResource) != null) { 1502 ids.add(getResourceToIdMap().get(nextResource).getValue()); 1503 } 1504 } 1505 1506 // Automatically assign IDs to the rest 1507 for (IBaseResource nextResource : getResourceList()) { 1508 1509 while (getResourceToIdMap().get(nextResource) == null) { 1510 String nextCandidate = "#" + myNextContainedId; 1511 myNextContainedId++; 1512 if (!ids.add(nextCandidate)) { 1513 continue; 1514 } 1515 1516 getResourceToIdMap().put(nextResource, new IdDt(nextCandidate)); 1517 } 1518 1519 } 1520 1521 } 1522 1523 } 1524 } 1525 1526 protected static <T> List<T> extractMetadataListNotNull(IResource resource, ResourceMetadataKeyEnum<List<T>> key) { 1527 List<? extends T> securityLabels = key.get(resource); 1528 if (securityLabels == null) { 1529 securityLabels = Collections.emptyList(); 1530 } 1531 return new ArrayList<>(securityLabels); 1532 } 1533 1534 static boolean hasNoExtensions(IBase theElement) { 1535 if (theElement instanceof ISupportsUndeclaredExtensions) { 1536 ISupportsUndeclaredExtensions res = (ISupportsUndeclaredExtensions) theElement; 1537 if (res.getUndeclaredExtensions().size() > 0 || res.getUndeclaredModifierExtensions().size() > 0) { 1538 return false; 1539 } 1540 } 1541 if (theElement instanceof IBaseHasExtensions) { 1542 IBaseHasExtensions res = (IBaseHasExtensions) theElement; 1543 if (res.hasExtension()) { 1544 return false; 1545 } 1546 } 1547 if (theElement instanceof IBaseHasModifierExtensions) { 1548 IBaseHasModifierExtensions res = (IBaseHasModifierExtensions) theElement; 1549 return !res.hasModifierExtension(); 1550 } 1551 return true; 1552 } 1553 1554}