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