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