001/* 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2023 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) && isBlank(myResourceType) && isBlank(myUnqualifiedId) && isBlank(myUnqualifiedVersionId)) { 186 myHaveComponentParts = false; 187 } else { 188 myHaveComponentParts = true; 189 } 190 } 191 192 @Override 193 public void applyTo(IBaseResource theResouce) { 194 if (theResouce == null) { 195 throw new NullPointerException(Msg.code(1875) + "theResource can not be null"); 196 } else if (theResouce instanceof IResource) { 197 ((IResource) theResouce).setId(new IdDt(getValue())); 198 } else if (theResouce instanceof IAnyResource) { 199 ((IAnyResource) theResouce).setId(getValue()); 200 } else { 201 throw new IllegalArgumentException(Msg.code(1876) + "Unknown resource class type, does not implement IResource or extend Resource"); 202 } 203 } 204 205 /** 206 * @deprecated Use {@link #getIdPartAsBigDecimal()} instead (this method was deprocated because its name is ambiguous) 207 */ 208 @Deprecated 209 public BigDecimal asBigDecimal() { 210 return getIdPartAsBigDecimal(); 211 } 212 213 @Override 214 public boolean equals(Object theArg0) { 215 if (!(theArg0 instanceof IdDt)) { 216 return false; 217 } 218 IdDt id = (IdDt) theArg0; 219 return StringUtils.equals(getValueAsString(), id.getValueAsString()); 220 } 221 222 /** 223 * Returns true if this IdDt matches the given IdDt in terms of resource type and ID, but ignores the URL base 224 */ 225 @SuppressWarnings("deprecation") 226 public boolean equalsIgnoreBase(IdDt theId) { 227 if (theId == null) { 228 return false; 229 } 230 if (theId.isEmpty()) { 231 return isEmpty(); 232 } 233 return ObjectUtils.equals(getResourceType(), theId.getResourceType()) && ObjectUtils.equals(getIdPart(), theId.getIdPart()) && ObjectUtils.equals(getVersionIdPart(), theId.getVersionIdPart()); 234 } 235 236 /** 237 * 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 238 * <code>http://example.com/fhir</code>. 239 * <p> 240 * This method may return null if the ID contains no base (e.g. "Patient/123") 241 * </p> 242 */ 243 @Override 244 public String getBaseUrl() { 245 return myBaseUrl; 246 } 247 248 /** 249 * 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". 250 */ 251 @Override 252 public String getIdPart() { 253 return myUnqualifiedId; 254 } 255 256 /** 257 * Returns the unqualified portion of this ID as a big decimal, or <code>null</code> if the value is null 258 * 259 * @throws NumberFormatException If the value is not a valid BigDecimal 260 */ 261 public BigDecimal getIdPartAsBigDecimal() { 262 String val = getIdPart(); 263 if (isBlank(val)) { 264 return null; 265 } 266 return new BigDecimal(val); 267 } 268 269 /** 270 * Returns the unqualified portion of this ID as a {@link Long}, or <code>null</code> if the value is null 271 * 272 * @throws NumberFormatException If the value is not a valid Long 273 */ 274 @Override 275 public Long getIdPartAsLong() { 276 String val = getIdPart(); 277 if (isBlank(val)) { 278 return null; 279 } 280 return Long.parseLong(val); 281 } 282 283 @Override 284 public String getResourceType() { 285 return myResourceType; 286 } 287 288 289 /** 290 * 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. 291 * 292 * @see #getIdPart() 293 */ 294 @Override 295 public String getValue() { 296 if (super.getValue() == null && myHaveComponentParts) { 297 298 if (isLocal() || isUrn()) { 299 return myUnqualifiedId; 300 } 301 302 StringBuilder b = new StringBuilder(); 303 if (isNotBlank(myBaseUrl)) { 304 b.append(myBaseUrl); 305 if (myBaseUrl.charAt(myBaseUrl.length() - 1) != '/') { 306 b.append('/'); 307 } 308 } 309 310 if (isNotBlank(myResourceType)) { 311 b.append(myResourceType); 312 } 313 314 if (b.length() > 0 && isNotBlank(myUnqualifiedId)) { 315 b.append('/'); 316 } 317 318 if (isNotBlank(myUnqualifiedId)) { 319 b.append(myUnqualifiedId); 320 } else if (isNotBlank(myUnqualifiedVersionId)) { 321 b.append('/'); 322 } 323 324 if (isNotBlank(myUnqualifiedVersionId)) { 325 b.append('/'); 326 b.append(Constants.PARAM_HISTORY); 327 b.append('/'); 328 b.append(myUnqualifiedVersionId); 329 } 330 String value = b.toString(); 331 super.setValue(value); 332 } 333 return super.getValue(); 334 } 335 336 /** 337 * Set the value 338 * <p> 339 * <p> 340 * <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 341 * limit of 36 characters. 342 * </p> 343 * <p> 344 * regex: [a-z0-9\-\.]{1,36} 345 * </p> 346 */ 347 @Override 348 public IdDt setValue(String theValue) throws DataFormatException { 349 // TODO: add validation 350 super.setValue(theValue); 351 myHaveComponentParts = false; 352 353 if (StringUtils.isBlank(theValue)) { 354 myBaseUrl = null; 355 super.setValue(null); 356 myUnqualifiedId = null; 357 myUnqualifiedVersionId = null; 358 myResourceType = null; 359 } else if (theValue.charAt(0) == '#' && theValue.length() > 1) { 360 super.setValue(theValue); 361 myBaseUrl = null; 362 myUnqualifiedId = theValue; 363 myUnqualifiedVersionId = null; 364 myResourceType = null; 365 myHaveComponentParts = true; 366 } else if (theValue.startsWith("urn:")) { 367 myBaseUrl = null; 368 myUnqualifiedId = theValue; 369 myUnqualifiedVersionId = null; 370 myResourceType = null; 371 myHaveComponentParts = true; 372 } else { 373 int vidIndex = theValue.indexOf("/_history/"); 374 int idIndex; 375 if (vidIndex != -1) { 376 myUnqualifiedVersionId = theValue.substring(vidIndex + "/_history/".length()); 377 idIndex = theValue.lastIndexOf('/', vidIndex - 1); 378 myUnqualifiedId = theValue.substring(idIndex + 1, vidIndex); 379 } else { 380 idIndex = theValue.lastIndexOf('/'); 381 myUnqualifiedId = theValue.substring(idIndex + 1); 382 myUnqualifiedVersionId = null; 383 } 384 385 myBaseUrl = null; 386 if (idIndex <= 0) { 387 myResourceType = null; 388 } else { 389 int typeIndex = theValue.lastIndexOf('/', idIndex - 1); 390 if (typeIndex == -1) { 391 myResourceType = theValue.substring(0, idIndex); 392 } else { 393 if (typeIndex > 0 && '/' == theValue.charAt(typeIndex - 1)) { 394 typeIndex = theValue.indexOf('/', typeIndex + 1); 395 } 396 if (typeIndex >= idIndex) { 397 // e.g. http://example.org/foo 398 // 'foo' was the id but we're making that the resource type. Nullify the id part because we don't have an id. 399 // Also set null value to the super.setValue() and enable myHaveComponentParts so it forces getValue() to properly 400 // recreate the url 401 myResourceType = myUnqualifiedId; 402 myUnqualifiedId = null; 403 super.setValue(null); 404 myHaveComponentParts = true; 405 } else { 406 myResourceType = theValue.substring(typeIndex + 1, idIndex); 407 } 408 409 if (typeIndex > 4) { 410 myBaseUrl = theValue.substring(0, typeIndex); 411 } 412 413 } 414 } 415 416 } 417 return this; 418 } 419 420 @Override 421 public String getValueAsString() { 422 return getValue(); 423 } 424 425 /** 426 * Set the value 427 * <p> 428 * <p> 429 * <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 430 * limit of 36 characters. 431 * </p> 432 * <p> 433 * regex: [a-z0-9\-\.]{1,36} 434 * </p> 435 */ 436 @Override 437 public void setValueAsString(String theValue) throws DataFormatException { 438 setValue(theValue); 439 } 440 441 @Override 442 public String getVersionIdPart() { 443 return myUnqualifiedVersionId; 444 } 445 446 @Override 447 public Long getVersionIdPartAsLong() { 448 if (!hasVersionIdPart()) { 449 return null; 450 } 451 return Long.parseLong(getVersionIdPart()); 452 } 453 454 /** 455 * Returns true if this ID has a base url 456 * 457 * @see #getBaseUrl() 458 */ 459 @Override 460 public boolean hasBaseUrl() { 461 return isNotBlank(myBaseUrl); 462 } 463 464 @Override 465 public boolean hasIdPart() { 466 return isNotBlank(getIdPart()); 467 } 468 469 @Override 470 public boolean hasResourceType() { 471 return isNotBlank(myResourceType); 472 } 473 474 @Override 475 public boolean hasVersionIdPart() { 476 return isNotBlank(getVersionIdPart()); 477 } 478 479 @Override 480 public int hashCode() { 481 HashCodeBuilder b = new HashCodeBuilder(); 482 b.append(getValueAsString()); 483 return b.toHashCode(); 484 } 485 486 /** 487 * Returns <code>true</code> if this ID contains an absolute URL (in other words, a URL starting with "http://" or "https://" 488 */ 489 @Override 490 public boolean isAbsolute() { 491 if (StringUtils.isBlank(getValue())) { 492 return false; 493 } 494 return UrlUtil.isAbsolute(getValue()); 495 } 496 497 @Override 498 public boolean isEmpty() { 499 return super.isBaseEmpty() && isBlank(getValue()); 500 } 501 502 @Override 503 public boolean isIdPartValid() { 504 String id = getIdPart(); 505 if (StringUtils.isBlank(id)) { 506 return false; 507 } 508 if (id.length() > 64) { 509 return false; 510 } 511 for (int i = 0; i < id.length(); i++) { 512 char nextChar = id.charAt(i); 513 if (nextChar >= 'a' && nextChar <= 'z') { 514 continue; 515 } 516 if (nextChar >= 'A' && nextChar <= 'Z') { 517 continue; 518 } 519 if (nextChar >= '0' && nextChar <= '9') { 520 continue; 521 } 522 if (nextChar == '-' || nextChar == '.') { 523 continue; 524 } 525 return false; 526 } 527 return true; 528 } 529 530 @Override 531 public boolean isIdPartValidLong() { 532 return isValidLong(getIdPart()); 533 } 534 535 /** 536 * Returns <code>true</code> if the ID is a local reference (in other words, 537 * it begins with the '#' character) 538 */ 539 @Override 540 public boolean isLocal() { 541 return defaultString(myUnqualifiedId).startsWith("#"); 542 } 543 544 private boolean isUrn() { 545 return defaultString(myUnqualifiedId).startsWith("urn:"); 546 } 547 548 @Override 549 public boolean isVersionIdPartValidLong() { 550 return isValidLong(getVersionIdPart()); 551 } 552 553 /** 554 * 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. 555 * 556 * @deprecated 557 */ 558 @Deprecated //override deprecated method 559 @Override 560 public void setId(IdDt theId) { 561 setValue(theId.getValue()); 562 } 563 564 @Override 565 public IIdType setParts(String theBaseUrl, String theResourceType, String theIdPart, String theVersionIdPart) { 566 if (isNotBlank(theVersionIdPart)) { 567 Validate.notBlank(theResourceType, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated"); 568 Validate.notBlank(theIdPart, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated"); 569 } 570 if (isNotBlank(theBaseUrl) && isNotBlank(theIdPart)) { 571 Validate.notBlank(theResourceType, "If theBaseUrl is populated and theIdPart is populated, theResourceType must be populated"); 572 } 573 574 setValue(null); 575 576 myBaseUrl = theBaseUrl; 577 myResourceType = theResourceType; 578 myUnqualifiedId = theIdPart; 579 myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionIdPart, null); 580 myHaveComponentParts = true; 581 582 return this; 583 } 584 585 @Override 586 public String toString() { 587 return getValue(); 588 } 589 590 /** 591 * 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 592 * return a new IdDt containing ID "Patient/1". 593 */ 594 @Override 595 public IdDt toUnqualified() { 596 if (isLocal() || isUrn()) { 597 return new IdDt(getValueAsString()); 598 } 599 return new IdDt(getResourceType(), getIdPart(), getVersionIdPart()); 600 } 601 602 @Override 603 public IdDt toUnqualifiedVersionless() { 604 if (isLocal() || isUrn()) { 605 return new IdDt(getValueAsString()); 606 } 607 return new IdDt(getResourceType(), getIdPart()); 608 } 609 610 @Override 611 public IdDt toVersionless() { 612 if (isLocal() || isUrn()) { 613 return new IdDt(getValueAsString()); 614 } 615 return new IdDt(getBaseUrl(), getResourceType(), getIdPart(), null); 616 } 617 618 @Override 619 public IdDt withResourceType(String theResourceName) { 620 if (isLocal() || isUrn()) { 621 return new IdDt(getValueAsString()); 622 } 623 return new IdDt(theResourceName, getIdPart(), getVersionIdPart()); 624 } 625 626 /** 627 * 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, 628 * 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. 629 * 630 * @param theServerBase The server base (e.g. "http://example.com/fhir") 631 * @param theResourceType The resource name (e.g. "Patient") 632 * @return A fully qualified URL for this ID (e.g. "http://example.com/fhir/Patient/1") 633 */ 634 @Override 635 public IdDt withServerBase(String theServerBase, String theResourceType) { 636 if (isLocal() || isUrn()) { 637 return new IdDt(getValueAsString()); 638 } 639 return new IdDt(theServerBase, theResourceType, getIdPart(), getVersionIdPart()); 640 } 641 642 /** 643 * Creates a new instance of this ID which is identical, but refers to the specific version of this resource ID noted by theVersion. 644 * 645 * @param theVersion The actual version string, e.g. "1". If theVersion is blank or null, returns the same as {@link #toVersionless()}} 646 * @return A new instance of IdDt which is identical, but refers to the specific version of this resource ID noted by theVersion. 647 */ 648 @Override 649 public IdDt withVersion(String theVersion) { 650 if (isBlank(theVersion)) { 651 return toVersionless(); 652 } 653 654 if (isLocal() || isUrn()) { 655 return new IdDt(getValueAsString()); 656 } 657 658 String existingValue = getValue(); 659 660 int i = existingValue.indexOf(Constants.PARAM_HISTORY); 661 String value; 662 if (i > 1) { 663 value = existingValue.substring(0, i - 1); 664 } else { 665 value = existingValue; 666 } 667 668 IdDt retval = new IdDt(this); 669 retval.myUnqualifiedVersionId = theVersion; 670 return retval; 671 } 672 673 public static boolean isValidLong(String id) { 674 return StringUtils.isNumeric(id); 675 } 676 677 /** 678 * Construct a new ID with with form "urn:uuid:[UUID]" where [UUID] is a new, randomly 679 * created UUID generated by {@link UUID#randomUUID()} 680 */ 681 public static IdDt newRandomUuid() { 682 return new IdDt("urn:uuid:" + UUID.randomUUID().toString()); 683 } 684 685 /** 686 * Retrieves the ID from the given resource instance 687 */ 688 public static IdDt of(IBaseResource theResouce) { 689 if (theResouce == null) { 690 throw new NullPointerException(Msg.code(1877) + "theResource can not be null"); 691 } 692 IIdType retVal = theResouce.getIdElement(); 693 if (retVal == null) { 694 return null; 695 } else if (retVal instanceof IdDt) { 696 return (IdDt) retVal; 697 } else { 698 return new IdDt(retVal.getValue()); 699 } 700 } 701 702 private static String toPlainStringWithNpeThrowIfNeeded(BigDecimal theIdPart) { 703 if (theIdPart == null) { 704 throw new NullPointerException(Msg.code(1878) + "BigDecimal ID can not be null"); 705 } 706 return theIdPart.toPlainString(); 707 } 708 709 private static String toPlainStringWithNpeThrowIfNeeded(Long theIdPart) { 710 if (theIdPart == null) { 711 throw new NullPointerException(Msg.code(1879) + "Long ID can not be null"); 712 } 713 return theIdPart.toString(); 714 } 715 716}