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