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