001package ca.uhn.fhir.model.primitive; 002 003import ca.uhn.fhir.model.api.IResource; 004import ca.uhn.fhir.model.api.annotation.DatatypeDef; 005import ca.uhn.fhir.model.api.annotation.SimpleSetter; 006import ca.uhn.fhir.parser.DataFormatException; 007import ca.uhn.fhir.rest.api.Constants; 008import ca.uhn.fhir.util.UrlUtil; 009import org.apache.commons.lang3.ObjectUtils; 010import org.apache.commons.lang3.StringUtils; 011import org.apache.commons.lang3.Validate; 012import org.apache.commons.lang3.builder.HashCodeBuilder; 013import org.hl7.fhir.instance.model.api.IAnyResource; 014import org.hl7.fhir.instance.model.api.IBaseResource; 015import org.hl7.fhir.instance.model.api.IIdType; 016 017import java.math.BigDecimal; 018import java.util.UUID; 019 020import static org.apache.commons.lang3.StringUtils.defaultString; 021import static org.apache.commons.lang3.StringUtils.isBlank; 022import static org.apache.commons.lang3.StringUtils.isNotBlank; 023 024/* 025 * #%L 026 * HAPI FHIR - Core Library 027 * %% 028 * Copyright (C) 2014 - 2021 Smile CDR, Inc. 029 * %% 030 * Licensed under the Apache License, Version 2.0 (the "License"); 031 * you may not use this file except in compliance with the License. 032 * You may obtain a copy of the License at 033 * 034 * http://www.apache.org/licenses/LICENSE-2.0 035 * 036 * Unless required by applicable law or agreed to in writing, software 037 * distributed under the License is distributed on an "AS IS" BASIS, 038 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 039 * See the License for the specific language governing permissions and 040 * limitations under the License. 041 * #L% 042 */ 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("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("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 * 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. 290 * 291 * @see #getIdPart() 292 */ 293 @Override 294 public String getValue() { 295 if (super.getValue() == null && myHaveComponentParts) { 296 297 if (isLocal() || isUrn()) { 298 return myUnqualifiedId; 299 } 300 301 StringBuilder b = new StringBuilder(); 302 if (isNotBlank(myBaseUrl)) { 303 b.append(myBaseUrl); 304 if (myBaseUrl.charAt(myBaseUrl.length() - 1) != '/') { 305 b.append('/'); 306 } 307 } 308 309 if (isNotBlank(myResourceType)) { 310 b.append(myResourceType); 311 } 312 313 if (b.length() > 0 && isNotBlank(myUnqualifiedId)) { 314 b.append('/'); 315 } 316 317 if (isNotBlank(myUnqualifiedId)) { 318 b.append(myUnqualifiedId); 319 } else if (isNotBlank(myUnqualifiedVersionId)) { 320 b.append('/'); 321 } 322 323 if (isNotBlank(myUnqualifiedVersionId)) { 324 b.append('/'); 325 b.append(Constants.PARAM_HISTORY); 326 b.append('/'); 327 b.append(myUnqualifiedVersionId); 328 } 329 String value = b.toString(); 330 super.setValue(value); 331 } 332 return super.getValue(); 333 } 334 335 /** 336 * Set the value 337 * <p> 338 * <p> 339 * <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 340 * limit of 36 characters. 341 * </p> 342 * <p> 343 * regex: [a-z0-9\-\.]{1,36} 344 * </p> 345 */ 346 @Override 347 public IdDt setValue(String theValue) throws DataFormatException { 348 // TODO: add validation 349 super.setValue(theValue); 350 myHaveComponentParts = false; 351 352 if (StringUtils.isBlank(theValue)) { 353 myBaseUrl = null; 354 super.setValue(null); 355 myUnqualifiedId = null; 356 myUnqualifiedVersionId = null; 357 myResourceType = null; 358 } else if (theValue.charAt(0) == '#' && theValue.length() > 1) { 359 super.setValue(theValue); 360 myBaseUrl = null; 361 myUnqualifiedId = theValue; 362 myUnqualifiedVersionId = null; 363 myResourceType = null; 364 myHaveComponentParts = true; 365 } else if (theValue.startsWith("urn:")) { 366 myBaseUrl = null; 367 myUnqualifiedId = theValue; 368 myUnqualifiedVersionId = null; 369 myResourceType = null; 370 myHaveComponentParts = true; 371 } else { 372 int vidIndex = theValue.indexOf("/_history/"); 373 int idIndex; 374 if (vidIndex != -1) { 375 myUnqualifiedVersionId = theValue.substring(vidIndex + "/_history/".length()); 376 idIndex = theValue.lastIndexOf('/', vidIndex - 1); 377 myUnqualifiedId = theValue.substring(idIndex + 1, vidIndex); 378 } else { 379 idIndex = theValue.lastIndexOf('/'); 380 myUnqualifiedId = theValue.substring(idIndex + 1); 381 myUnqualifiedVersionId = null; 382 } 383 384 myBaseUrl = null; 385 if (idIndex <= 0) { 386 myResourceType = null; 387 } else { 388 int typeIndex = theValue.lastIndexOf('/', idIndex - 1); 389 if (typeIndex == -1) { 390 myResourceType = theValue.substring(0, idIndex); 391 } else { 392 if (typeIndex > 0 && '/' == theValue.charAt(typeIndex - 1)) { 393 typeIndex = theValue.indexOf('/', typeIndex + 1); 394 } 395 if (typeIndex >= idIndex) { 396 // e.g. http://example.org/foo 397 // 'foo' was the id but we're making that the resource type. Nullify the id part because we don't have an id. 398 // Also set null value to the super.setValue() and enable myHaveComponentParts so it forces getValue() to properly 399 // recreate the url 400 myResourceType = myUnqualifiedId; 401 myUnqualifiedId = null; 402 super.setValue(null); 403 myHaveComponentParts = true; 404 } else { 405 myResourceType = theValue.substring(typeIndex + 1, idIndex); 406 } 407 408 if (typeIndex > 4) { 409 myBaseUrl = theValue.substring(0, typeIndex); 410 } 411 412 } 413 } 414 415 } 416 return this; 417 } 418 419 @Override 420 public String getValueAsString() { 421 return getValue(); 422 } 423 424 /** 425 * Set the value 426 * <p> 427 * <p> 428 * <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 429 * limit of 36 characters. 430 * </p> 431 * <p> 432 * regex: [a-z0-9\-\.]{1,36} 433 * </p> 434 */ 435 @Override 436 public void setValueAsString(String theValue) throws DataFormatException { 437 setValue(theValue); 438 } 439 440 @Override 441 public String getVersionIdPart() { 442 return myUnqualifiedVersionId; 443 } 444 445 @Override 446 public Long getVersionIdPartAsLong() { 447 if (!hasVersionIdPart()) { 448 return null; 449 } 450 return Long.parseLong(getVersionIdPart()); 451 } 452 453 /** 454 * Returns true if this ID has a base url 455 * 456 * @see #getBaseUrl() 457 */ 458 @Override 459 public boolean hasBaseUrl() { 460 return isNotBlank(myBaseUrl); 461 } 462 463 @Override 464 public boolean hasIdPart() { 465 return isNotBlank(getIdPart()); 466 } 467 468 @Override 469 public boolean hasResourceType() { 470 return isNotBlank(myResourceType); 471 } 472 473 @Override 474 public boolean hasVersionIdPart() { 475 return isNotBlank(getVersionIdPart()); 476 } 477 478 @Override 479 public int hashCode() { 480 HashCodeBuilder b = new HashCodeBuilder(); 481 b.append(getValueAsString()); 482 return b.toHashCode(); 483 } 484 485 /** 486 * Returns <code>true</code> if this ID contains an absolute URL (in other words, a URL starting with "http://" or "https://" 487 */ 488 @Override 489 public boolean isAbsolute() { 490 if (StringUtils.isBlank(getValue())) { 491 return false; 492 } 493 return UrlUtil.isAbsolute(getValue()); 494 } 495 496 @Override 497 public boolean isEmpty() { 498 return super.isBaseEmpty() && isBlank(getValue()); 499 } 500 501 @Override 502 public boolean isIdPartValid() { 503 String id = getIdPart(); 504 if (StringUtils.isBlank(id)) { 505 return false; 506 } 507 if (id.length() > 64) { 508 return false; 509 } 510 for (int i = 0; i < id.length(); i++) { 511 char nextChar = id.charAt(i); 512 if (nextChar >= 'a' && nextChar <= 'z') { 513 continue; 514 } 515 if (nextChar >= 'A' && nextChar <= 'Z') { 516 continue; 517 } 518 if (nextChar >= '0' && nextChar <= '9') { 519 continue; 520 } 521 if (nextChar == '-' || nextChar == '.') { 522 continue; 523 } 524 return false; 525 } 526 return true; 527 } 528 529 @Override 530 public boolean isIdPartValidLong() { 531 return isValidLong(getIdPart()); 532 } 533 534 /** 535 * Returns <code>true</code> if the ID is a local reference (in other words, 536 * it begins with the '#' character) 537 */ 538 @Override 539 public boolean isLocal() { 540 return defaultString(myUnqualifiedId).startsWith("#"); 541 } 542 543 private boolean isUrn() { 544 return defaultString(myUnqualifiedId).startsWith("urn:"); 545 } 546 547 @Override 548 public boolean isVersionIdPartValidLong() { 549 return isValidLong(getVersionIdPart()); 550 } 551 552 /** 553 * 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. 554 * 555 * @deprecated 556 */ 557 @Deprecated //override deprecated method 558 @Override 559 public void setId(IdDt theId) { 560 setValue(theId.getValue()); 561 } 562 563 @Override 564 public IIdType setParts(String theBaseUrl, String theResourceType, String theIdPart, String theVersionIdPart) { 565 if (isNotBlank(theVersionIdPart)) { 566 Validate.notBlank(theResourceType, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated"); 567 Validate.notBlank(theIdPart, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated"); 568 } 569 if (isNotBlank(theBaseUrl) && isNotBlank(theIdPart)) { 570 Validate.notBlank(theResourceType, "If theBaseUrl is populated and theIdPart is populated, theResourceType must be populated"); 571 } 572 573 setValue(null); 574 575 myBaseUrl = theBaseUrl; 576 myResourceType = theResourceType; 577 myUnqualifiedId = theIdPart; 578 myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionIdPart, null); 579 myHaveComponentParts = true; 580 581 return this; 582 } 583 584 @Override 585 public String toString() { 586 return getValue(); 587 } 588 589 /** 590 * 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 591 * return a new IdDt containing ID "Patient/1". 592 */ 593 @Override 594 public IdDt toUnqualified() { 595 if (isLocal() || isUrn()) { 596 return new IdDt(getValueAsString()); 597 } 598 return new IdDt(getResourceType(), getIdPart(), getVersionIdPart()); 599 } 600 601 @Override 602 public IdDt toUnqualifiedVersionless() { 603 if (isLocal() || isUrn()) { 604 return new IdDt(getValueAsString()); 605 } 606 return new IdDt(getResourceType(), getIdPart()); 607 } 608 609 @Override 610 public IdDt toVersionless() { 611 if (isLocal() || isUrn()) { 612 return new IdDt(getValueAsString()); 613 } 614 return new IdDt(getBaseUrl(), getResourceType(), getIdPart(), null); 615 } 616 617 @Override 618 public IdDt withResourceType(String theResourceName) { 619 if (isLocal() || isUrn()) { 620 return new IdDt(getValueAsString()); 621 } 622 return new IdDt(theResourceName, getIdPart(), getVersionIdPart()); 623 } 624 625 /** 626 * 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, 627 * 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. 628 * 629 * @param theServerBase The server base (e.g. "http://example.com/fhir") 630 * @param theResourceType The resource name (e.g. "Patient") 631 * @return A fully qualified URL for this ID (e.g. "http://example.com/fhir/Patient/1") 632 */ 633 @Override 634 public IdDt withServerBase(String theServerBase, String theResourceType) { 635 if (isLocal() || isUrn()) { 636 return new IdDt(getValueAsString()); 637 } 638 return new IdDt(theServerBase, theResourceType, getIdPart(), getVersionIdPart()); 639 } 640 641 /** 642 * Creates a new instance of this ID which is identical, but refers to the specific version of this resource ID noted by theVersion. 643 * 644 * @param theVersion The actual version string, e.g. "1". If theVersion is blank or null, returns the same as {@link #toVersionless()}} 645 * @return A new instance of IdDt which is identical, but refers to the specific version of this resource ID noted by theVersion. 646 */ 647 @Override 648 public IdDt withVersion(String theVersion) { 649 if (isBlank(theVersion)) { 650 return toVersionless(); 651 } 652 653 if (isLocal() || isUrn()) { 654 return new IdDt(getValueAsString()); 655 } 656 657 String existingValue = getValue(); 658 659 int i = existingValue.indexOf(Constants.PARAM_HISTORY); 660 String value; 661 if (i > 1) { 662 value = existingValue.substring(0, i - 1); 663 } else { 664 value = existingValue; 665 } 666 667 IdDt retval = new IdDt(this); 668 retval.myUnqualifiedVersionId = theVersion; 669 return retval; 670 } 671 672 public static boolean isValidLong(String id) { 673 return StringUtils.isNumeric(id); 674 } 675 676 /** 677 * Construct a new ID with with form "urn:uuid:[UUID]" where [UUID] is a new, randomly 678 * created UUID generated by {@link UUID#randomUUID()} 679 */ 680 public static IdDt newRandomUuid() { 681 return new IdDt("urn:uuid:" + UUID.randomUUID().toString()); 682 } 683 684 /** 685 * Retrieves the ID from the given resource instance 686 */ 687 public static IdDt of(IBaseResource theResouce) { 688 if (theResouce == null) { 689 throw new NullPointerException("theResource can not be null"); 690 } 691 IIdType retVal = theResouce.getIdElement(); 692 if (retVal == null) { 693 return null; 694 } else if (retVal instanceof IdDt) { 695 return (IdDt) retVal; 696 } else { 697 return new IdDt(retVal.getValue()); 698 } 699 } 700 701 private static String toPlainStringWithNpeThrowIfNeeded(BigDecimal theIdPart) { 702 if (theIdPart == null) { 703 throw new NullPointerException("BigDecimal ID can not be null"); 704 } 705 return theIdPart.toPlainString(); 706 } 707 708 private static String toPlainStringWithNpeThrowIfNeeded(Long theIdPart) { 709 if (theIdPart == null) { 710 throw new NullPointerException("Long ID can not be null"); 711 } 712 return theIdPart.toString(); 713 } 714 715}