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