001/* 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.model.primitive; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.model.api.IResource; 024import ca.uhn.fhir.model.api.annotation.DatatypeDef; 025import ca.uhn.fhir.model.api.annotation.SimpleSetter; 026import ca.uhn.fhir.parser.DataFormatException; 027import ca.uhn.fhir.rest.api.Constants; 028import ca.uhn.fhir.util.UrlUtil; 029import org.apache.commons.lang3.ObjectUtils; 030import org.apache.commons.lang3.StringUtils; 031import org.apache.commons.lang3.Validate; 032import org.apache.commons.lang3.builder.HashCodeBuilder; 033import org.hl7.fhir.instance.model.api.IAnyResource; 034import org.hl7.fhir.instance.model.api.IBaseResource; 035import org.hl7.fhir.instance.model.api.IIdType; 036 037import java.math.BigDecimal; 038import java.util.UUID; 039 040import static org.apache.commons.lang3.StringUtils.defaultString; 041import static org.apache.commons.lang3.StringUtils.isBlank; 042import static org.apache.commons.lang3.StringUtils.isNotBlank; 043 044/** 045 * Represents the FHIR ID type. This is the actual resource ID, meaning the ID that will be used in RESTful URLs, Resource References, etc. to represent a specific instance of a resource. 046 * <p> 047 * <p> 048 * <b>Description</b>: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length 049 * limit of 36 characters. 050 * </p> 051 * <p> 052 * regex: [a-z-Z0-9\-\.]{1,36} 053 * </p> 054 */ 055@DatatypeDef(name = "id", profileOf = StringDt.class) 056public class IdDt extends UriDt implements /*IPrimitiveDatatype<String>, */ IIdType { 057 058 private String myBaseUrl; 059 private boolean myHaveComponentParts; 060 private String myResourceType; 061 private String myUnqualifiedId; 062 private String myUnqualifiedVersionId; 063 064 /** 065 * Create a new empty ID 066 */ 067 public IdDt() { 068 super(); 069 } 070 071 /** 072 * Create a new ID, using a BigDecimal input. Uses {@link BigDecimal#toPlainString()} to generate the string representation. 073 */ 074 public IdDt(BigDecimal thePid) { 075 if (thePid != null) { 076 setValue(toPlainStringWithNpeThrowIfNeeded(thePid)); 077 } else { 078 setValue(null); 079 } 080 } 081 082 /** 083 * Create a new ID using a long 084 */ 085 public IdDt(long theId) { 086 setValue(Long.toString(theId)); 087 } 088 089 /** 090 * Create a new ID using a string. This String may contain a simple ID (e.g. "1234") or it may contain a complete URL (http://example.com/fhir/Patient/1234). 091 * <p> 092 * <p> 093 * <b>Description</b>: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length 094 * limit of 36 characters. 095 * </p> 096 * <p> 097 * regex: [a-z0-9\-\.]{1,36} 098 * </p> 099 */ 100 @SimpleSetter 101 public IdDt(@SimpleSetter.Parameter(name = "theId") String theValue) { 102 setValue(theValue); 103 } 104 105 /** 106 * Constructor 107 * 108 * @param theResourceType The resource type (e.g. "Patient") 109 * @param theIdPart The ID (e.g. "123") 110 */ 111 public IdDt(String theResourceType, BigDecimal theIdPart) { 112 this(theResourceType, toPlainStringWithNpeThrowIfNeeded(theIdPart)); 113 } 114 115 /** 116 * Constructor 117 * 118 * @param theResourceType The resource type (e.g. "Patient") 119 * @param theIdPart The ID (e.g. "123") 120 */ 121 public IdDt(String theResourceType, Long theIdPart) { 122 this(theResourceType, toPlainStringWithNpeThrowIfNeeded(theIdPart)); 123 } 124 125 /** 126 * Constructor 127 * 128 * @param theResourceType The resource type (e.g. "Patient") 129 * @param theId The ID (e.g. "123") 130 */ 131 public IdDt(String theResourceType, String theId) { 132 this(theResourceType, theId, null); 133 } 134 135 /** 136 * Constructor 137 * 138 * @param theResourceType The resource type (e.g. "Patient") 139 * @param theId The ID (e.g. "123") 140 * @param theVersionId The version ID ("e.g. "456") 141 */ 142 public IdDt(String theResourceType, String theId, String theVersionId) { 143 this(null, theResourceType, theId, theVersionId); 144 } 145 146 /** 147 * Constructor 148 * 149 * @param theBaseUrl The server base URL (e.g. "http://example.com/fhir") 150 * @param theResourceType The resource type (e.g. "Patient") 151 * @param theId The ID (e.g. "123") 152 * @param theVersionId The version ID ("e.g. "456") 153 */ 154 public IdDt(String theBaseUrl, String theResourceType, String theId, String theVersionId) { 155 myBaseUrl = theBaseUrl; 156 myResourceType = theResourceType; 157 myUnqualifiedId = theId; 158 myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionId, null); 159 setHaveComponentParts(this); 160 } 161 162 public IdDt(IIdType theId) { 163 myBaseUrl = theId.getBaseUrl(); 164 myResourceType = theId.getResourceType(); 165 myUnqualifiedId = theId.getIdPart(); 166 myUnqualifiedVersionId = theId.getVersionIdPart(); 167 setHaveComponentParts(this); 168 } 169 170 /** 171 * Creates an ID based on a given URL 172 */ 173 public IdDt(UriDt theUrl) { 174 setValue(theUrl.getValueAsString()); 175 } 176 177 /** 178 * Copy Constructor 179 */ 180 public IdDt(IdDt theIdDt) { 181 this(theIdDt.myBaseUrl, theIdDt.myResourceType, theIdDt.myUnqualifiedId, theIdDt.myUnqualifiedVersionId); 182 } 183 184 private void setHaveComponentParts(IdDt theIdDt) { 185 if (isBlank(myBaseUrl) 186 && isBlank(myResourceType) 187 && isBlank(myUnqualifiedId) 188 && isBlank(myUnqualifiedVersionId)) { 189 myHaveComponentParts = false; 190 } else { 191 myHaveComponentParts = true; 192 } 193 } 194 195 @Override 196 public void applyTo(IBaseResource theResouce) { 197 if (theResouce == null) { 198 throw new NullPointerException(Msg.code(1875) + "theResource can not be null"); 199 } else if (theResouce instanceof IResource) { 200 ((IResource) theResouce).setId(new IdDt(getValue())); 201 } else if (theResouce instanceof IAnyResource) { 202 ((IAnyResource) theResouce).setId(getValue()); 203 } else { 204 throw new IllegalArgumentException( 205 Msg.code(1876) + "Unknown resource class type, does not implement IResource or extend Resource"); 206 } 207 } 208 209 /** 210 * @deprecated Use {@link #getIdPartAsBigDecimal()} instead (this method was deprocated because its name is ambiguous) 211 */ 212 @Deprecated 213 public BigDecimal asBigDecimal() { 214 return getIdPartAsBigDecimal(); 215 } 216 217 @Override 218 public boolean equals(Object theArg0) { 219 if (!(theArg0 instanceof IdDt)) { 220 return false; 221 } 222 IdDt id = (IdDt) theArg0; 223 return StringUtils.equals(getValueAsString(), id.getValueAsString()); 224 } 225 226 /** 227 * Returns true if this IdDt matches the given IdDt in terms of resource type and ID, but ignores the URL base 228 */ 229 @SuppressWarnings("deprecation") 230 public boolean equalsIgnoreBase(IdDt theId) { 231 if (theId == null) { 232 return false; 233 } 234 if (theId.isEmpty()) { 235 return isEmpty(); 236 } 237 return ObjectUtils.equals(getResourceType(), theId.getResourceType()) 238 && ObjectUtils.equals(getIdPart(), theId.getIdPart()) 239 && ObjectUtils.equals(getVersionIdPart(), theId.getVersionIdPart()); 240 } 241 242 /** 243 * Returns the portion of this resource ID which corresponds to the server base URL. For example given the resource ID <code>http://example.com/fhir/Patient/123</code> the base URL would be 244 * <code>http://example.com/fhir</code>. 245 * <p> 246 * This method may return null if the ID contains no base (e.g. "Patient/123") 247 * </p> 248 */ 249 @Override 250 public String getBaseUrl() { 251 return myBaseUrl; 252 } 253 254 /** 255 * Returns only the logical ID part of this ID. For example, given the ID "http://example,.com/fhir/Patient/123/_history/456", this method would return "123". 256 */ 257 @Override 258 public String getIdPart() { 259 return myUnqualifiedId; 260 } 261 262 /** 263 * Returns the unqualified portion of this ID as a big decimal, or <code>null</code> if the value is null 264 * 265 * @throws NumberFormatException If the value is not a valid BigDecimal 266 */ 267 public BigDecimal getIdPartAsBigDecimal() { 268 String val = getIdPart(); 269 if (isBlank(val)) { 270 return null; 271 } 272 return new BigDecimal(val); 273 } 274 275 /** 276 * Returns the unqualified portion of this ID as a {@link Long}, or <code>null</code> if the value is null 277 * 278 * @throws NumberFormatException If the value is not a valid Long 279 */ 280 @Override 281 public Long getIdPartAsLong() { 282 String val = getIdPart(); 283 if (isBlank(val)) { 284 return null; 285 } 286 return Long.parseLong(val); 287 } 288 289 @Override 290 public String getResourceType() { 291 return myResourceType; 292 } 293 294 /** 295 * Returns the value of this ID. Note that this value may be a fully qualified URL, a relative/partial URL, or a simple ID. Use {@link #getIdPart()} to get just the ID portion. 296 * 297 * @see #getIdPart() 298 */ 299 @Override 300 public String getValue() { 301 if (super.getValue() == null && myHaveComponentParts) { 302 303 if (isLocal() || isUrn()) { 304 return myUnqualifiedId; 305 } 306 307 StringBuilder b = new StringBuilder(); 308 if (isNotBlank(myBaseUrl)) { 309 b.append(myBaseUrl); 310 if (myBaseUrl.charAt(myBaseUrl.length() - 1) != '/') { 311 b.append('/'); 312 } 313 } 314 315 if (isNotBlank(myResourceType)) { 316 b.append(myResourceType); 317 } 318 319 if (b.length() > 0 && isNotBlank(myUnqualifiedId)) { 320 b.append('/'); 321 } 322 323 if (isNotBlank(myUnqualifiedId)) { 324 b.append(myUnqualifiedId); 325 } else if (isNotBlank(myUnqualifiedVersionId)) { 326 b.append('/'); 327 } 328 329 if (isNotBlank(myUnqualifiedVersionId)) { 330 b.append('/'); 331 b.append(Constants.PARAM_HISTORY); 332 b.append('/'); 333 b.append(myUnqualifiedVersionId); 334 } 335 String value = b.toString(); 336 super.setValue(value); 337 } 338 return super.getValue(); 339 } 340 341 /** 342 * Set the value 343 * <p> 344 * <p> 345 * <b>Description</b>: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length 346 * limit of 36 characters. 347 * </p> 348 * <p> 349 * regex: [a-z0-9\-\.]{1,36} 350 * </p> 351 */ 352 @Override 353 public IdDt setValue(String theValue) throws DataFormatException { 354 // TODO: add validation 355 super.setValue(theValue); 356 myHaveComponentParts = false; 357 358 if (StringUtils.isBlank(theValue)) { 359 myBaseUrl = null; 360 super.setValue(null); 361 myUnqualifiedId = null; 362 myUnqualifiedVersionId = null; 363 myResourceType = null; 364 } else if (theValue.charAt(0) == '#' && theValue.length() > 1) { 365 super.setValue(theValue); 366 myBaseUrl = null; 367 myUnqualifiedId = theValue; 368 myUnqualifiedVersionId = null; 369 myResourceType = null; 370 myHaveComponentParts = true; 371 } else if (theValue.startsWith("urn:")) { 372 myBaseUrl = null; 373 myUnqualifiedId = theValue; 374 myUnqualifiedVersionId = null; 375 myResourceType = null; 376 myHaveComponentParts = true; 377 } else { 378 int vidIndex = theValue.indexOf("/_history/"); 379 int idIndex; 380 if (vidIndex != -1) { 381 myUnqualifiedVersionId = theValue.substring(vidIndex + "/_history/".length()); 382 idIndex = theValue.lastIndexOf('/', vidIndex - 1); 383 myUnqualifiedId = theValue.substring(idIndex + 1, vidIndex); 384 } else { 385 idIndex = theValue.lastIndexOf('/'); 386 myUnqualifiedId = theValue.substring(idIndex + 1); 387 myUnqualifiedVersionId = null; 388 } 389 390 myBaseUrl = null; 391 if (idIndex <= 0) { 392 myResourceType = null; 393 } else { 394 int typeIndex = theValue.lastIndexOf('/', idIndex - 1); 395 if (typeIndex == -1) { 396 myResourceType = theValue.substring(0, idIndex); 397 } else { 398 if (typeIndex > 0 && '/' == theValue.charAt(typeIndex - 1)) { 399 typeIndex = theValue.indexOf('/', typeIndex + 1); 400 } 401 if (typeIndex >= idIndex) { 402 // e.g. http://example.org/foo 403 // 'foo' was the id but we're making that the resource type. Nullify the id part because we 404 // don't have an id. 405 // Also set null value to the super.setValue() and enable myHaveComponentParts so it forces 406 // getValue() to properly 407 // recreate the url 408 myResourceType = myUnqualifiedId; 409 myUnqualifiedId = null; 410 super.setValue(null); 411 myHaveComponentParts = true; 412 } else { 413 myResourceType = theValue.substring(typeIndex + 1, idIndex); 414 } 415 416 if (typeIndex > 4) { 417 myBaseUrl = theValue.substring(0, typeIndex); 418 } 419 } 420 } 421 } 422 return this; 423 } 424 425 @Override 426 public String getValueAsString() { 427 return getValue(); 428 } 429 430 /** 431 * Set the value 432 * <p> 433 * <p> 434 * <b>Description</b>: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length 435 * limit of 36 characters. 436 * </p> 437 * <p> 438 * regex: [a-z0-9\-\.]{1,36} 439 * </p> 440 */ 441 @Override 442 public void setValueAsString(String theValue) throws DataFormatException { 443 setValue(theValue); 444 } 445 446 @Override 447 public String getVersionIdPart() { 448 return myUnqualifiedVersionId; 449 } 450 451 @Override 452 public Long getVersionIdPartAsLong() { 453 if (!hasVersionIdPart()) { 454 return null; 455 } 456 return Long.parseLong(getVersionIdPart()); 457 } 458 459 /** 460 * Returns true if this ID has a base url 461 * 462 * @see #getBaseUrl() 463 */ 464 @Override 465 public boolean hasBaseUrl() { 466 return isNotBlank(myBaseUrl); 467 } 468 469 @Override 470 public boolean hasIdPart() { 471 return isNotBlank(getIdPart()); 472 } 473 474 @Override 475 public boolean hasResourceType() { 476 return isNotBlank(myResourceType); 477 } 478 479 @Override 480 public boolean hasVersionIdPart() { 481 return isNotBlank(getVersionIdPart()); 482 } 483 484 @Override 485 public int hashCode() { 486 HashCodeBuilder b = new HashCodeBuilder(); 487 b.append(getValueAsString()); 488 return b.toHashCode(); 489 } 490 491 /** 492 * Returns <code>true</code> if this ID contains an absolute URL (in other words, a URL starting with "http://" or "https://" 493 */ 494 @Override 495 public boolean isAbsolute() { 496 if (StringUtils.isBlank(getValue())) { 497 return false; 498 } 499 return UrlUtil.isAbsolute(getValue()); 500 } 501 502 @Override 503 public boolean isEmpty() { 504 return super.isBaseEmpty() && isBlank(getValue()); 505 } 506 507 @Override 508 public boolean isIdPartValid() { 509 String id = getIdPart(); 510 if (StringUtils.isBlank(id)) { 511 return false; 512 } 513 if (id.length() > 64) { 514 return false; 515 } 516 for (int i = 0; i < id.length(); i++) { 517 char nextChar = id.charAt(i); 518 if (nextChar >= 'a' && nextChar <= 'z') { 519 continue; 520 } 521 if (nextChar >= 'A' && nextChar <= 'Z') { 522 continue; 523 } 524 if (nextChar >= '0' && nextChar <= '9') { 525 continue; 526 } 527 if (nextChar == '-' || nextChar == '.') { 528 continue; 529 } 530 return false; 531 } 532 return true; 533 } 534 535 @Override 536 public boolean isIdPartValidLong() { 537 return isValidLong(getIdPart()); 538 } 539 540 /** 541 * Returns <code>true</code> if the ID is a local reference (in other words, 542 * it begins with the '#' character) 543 */ 544 @Override 545 public boolean isLocal() { 546 return defaultString(myUnqualifiedId).startsWith("#"); 547 } 548 549 private boolean isUrn() { 550 return defaultString(myUnqualifiedId).startsWith("urn:"); 551 } 552 553 @Override 554 public boolean isVersionIdPartValidLong() { 555 return isValidLong(getVersionIdPart()); 556 } 557 558 /** 559 * Copies the value from the given IdDt to <code>this</code> IdDt. It is generally not neccesary to use this method but it is provided for consistency with the rest of the API. 560 * 561 * @deprecated 562 */ 563 @Deprecated // override deprecated method 564 @Override 565 public void setId(IdDt theId) { 566 setValue(theId.getValue()); 567 } 568 569 @Override 570 public IIdType setParts(String theBaseUrl, String theResourceType, String theIdPart, String theVersionIdPart) { 571 if (isNotBlank(theVersionIdPart)) { 572 Validate.notBlank( 573 theResourceType, 574 "If theVersionIdPart is populated, theResourceType and theIdPart must be populated"); 575 Validate.notBlank( 576 theIdPart, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated"); 577 } 578 if (isNotBlank(theBaseUrl) && isNotBlank(theIdPart)) { 579 Validate.notBlank( 580 theResourceType, 581 "If theBaseUrl is populated and theIdPart is populated, theResourceType must be populated"); 582 } 583 584 setValue(null); 585 586 myBaseUrl = theBaseUrl; 587 myResourceType = theResourceType; 588 myUnqualifiedId = theIdPart; 589 myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionIdPart, null); 590 myHaveComponentParts = true; 591 592 return this; 593 } 594 595 @Override 596 public String toString() { 597 return getValue(); 598 } 599 600 /** 601 * Returns a new IdDt containing this IdDt's values but with no server base URL if one is present in this IdDt. For example, if this IdDt contains the ID "http://foo/Patient/1", this method will 602 * return a new IdDt containing ID "Patient/1". 603 */ 604 @Override 605 public IdDt toUnqualified() { 606 if (isLocal() || isUrn()) { 607 return new IdDt(getValueAsString()); 608 } 609 return new IdDt(getResourceType(), getIdPart(), getVersionIdPart()); 610 } 611 612 @Override 613 public IdDt toUnqualifiedVersionless() { 614 if (isLocal() || isUrn()) { 615 return new IdDt(getValueAsString()); 616 } 617 return new IdDt(getResourceType(), getIdPart()); 618 } 619 620 @Override 621 public IdDt toVersionless() { 622 if (isLocal() || isUrn()) { 623 return new IdDt(getValueAsString()); 624 } 625 return new IdDt(getBaseUrl(), getResourceType(), getIdPart(), null); 626 } 627 628 @Override 629 public IdDt withResourceType(String theResourceName) { 630 if (isLocal() || isUrn()) { 631 return new IdDt(getValueAsString()); 632 } 633 return new IdDt(theResourceName, getIdPart(), getVersionIdPart()); 634 } 635 636 /** 637 * Returns a view of this ID as a fully qualified URL, given a server base and resource name (which will only be used if the ID does not already contain those respective parts). Essentially, 638 * because IdDt can contain either a complete URL or a partial one (or even jut a simple ID), this method may be used to translate into a complete URL. 639 * 640 * @param theServerBase The server base (e.g. "http://example.com/fhir") 641 * @param theResourceType The resource name (e.g. "Patient") 642 * @return A fully qualified URL for this ID (e.g. "http://example.com/fhir/Patient/1") 643 */ 644 @Override 645 public IdDt withServerBase(String theServerBase, String theResourceType) { 646 if (isLocal() || isUrn()) { 647 return new IdDt(getValueAsString()); 648 } 649 return new IdDt(theServerBase, theResourceType, getIdPart(), getVersionIdPart()); 650 } 651 652 /** 653 * Creates a new instance of this ID which is identical, but refers to the specific version of this resource ID noted by theVersion. 654 * 655 * @param theVersion The actual version string, e.g. "1". If theVersion is blank or null, returns the same as {@link #toVersionless()}} 656 * @return A new instance of IdDt which is identical, but refers to the specific version of this resource ID noted by theVersion. 657 */ 658 @Override 659 public IdDt withVersion(String theVersion) { 660 if (isBlank(theVersion)) { 661 return toVersionless(); 662 } 663 664 if (isLocal() || isUrn()) { 665 return new IdDt(getValueAsString()); 666 } 667 668 String existingValue = getValue(); 669 670 int i = existingValue.indexOf(Constants.PARAM_HISTORY); 671 String value; 672 if (i > 1) { 673 value = existingValue.substring(0, i - 1); 674 } else { 675 value = existingValue; 676 } 677 678 IdDt retval = new IdDt(this); 679 retval.myUnqualifiedVersionId = theVersion; 680 return retval; 681 } 682 683 public static boolean isValidLong(String id) { 684 return StringUtils.isNumeric(id); 685 } 686 687 /** 688 * Construct a new ID with with form "urn:uuid:[UUID]" where [UUID] is a new, randomly 689 * created UUID generated by {@link UUID#randomUUID()} 690 */ 691 public static IdDt newRandomUuid() { 692 return new IdDt("urn:uuid:" + UUID.randomUUID().toString()); 693 } 694 695 /** 696 * Retrieves the ID from the given resource instance 697 */ 698 public static IdDt of(IBaseResource theResouce) { 699 if (theResouce == null) { 700 throw new NullPointerException(Msg.code(1877) + "theResource can not be null"); 701 } 702 IIdType retVal = theResouce.getIdElement(); 703 if (retVal == null) { 704 return null; 705 } else if (retVal instanceof IdDt) { 706 return (IdDt) retVal; 707 } else { 708 return new IdDt(retVal.getValue()); 709 } 710 } 711 712 private static String toPlainStringWithNpeThrowIfNeeded(BigDecimal theIdPart) { 713 if (theIdPart == null) { 714 throw new NullPointerException(Msg.code(1878) + "BigDecimal ID can not be null"); 715 } 716 return theIdPart.toPlainString(); 717 } 718 719 private static String toPlainStringWithNpeThrowIfNeeded(Long theIdPart) { 720 if (theIdPart == null) { 721 throw new NullPointerException(Msg.code(1879) + "Long ID can not be null"); 722 } 723 return theIdPart.toString(); 724 } 725}