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