package ca.uhn.fhir.model.primitive; import static org.apache.commons.lang3.StringUtils.defaultString; /* * #%L * HAPI FHIR - Core Library * %% * Copyright (C) 2014 - 2017 University Health Network * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.math.BigDecimal; import java.util.UUID; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.model.api.annotation.SimpleSetter; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.util.UrlUtil; /** * 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. * * <p> * <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 * limit of 36 characters. * </p> * <p> * regex: [a-z-Z0-9\-\.]{1,36} * </p> */ @DatatypeDef(name = "id", profileOf = StringDt.class) public class IdDt extends UriDt implements /*IPrimitiveDatatype<String>, */IIdType { private String myBaseUrl; private boolean myHaveComponentParts; private String myResourceType; private String myUnqualifiedId; private String myUnqualifiedVersionId; /** * Create a new empty ID */ public IdDt() { super(); } /** * Create a new ID, using a BigDecimal input. Uses {@link BigDecimal#toPlainString()} to generate the string representation. */ public IdDt(BigDecimal thePid) { if (thePid != null) { setValue(toPlainStringWithNpeThrowIfNeeded(thePid)); } else { setValue(null); } } /** * Create a new ID using a long */ public IdDt(long theId) { setValue(Long.toString(theId)); } /** * 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). * * <p> * <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 * limit of 36 characters. * </p> * <p> * regex: [a-z0-9\-\.]{1,36} * </p> */ @SimpleSetter public IdDt(@SimpleSetter.Parameter(name = "theId") String theValue) { setValue(theValue); } /** * Constructor * * @param theResourceType * The resource type (e.g. "Patient") * @param theIdPart * The ID (e.g. "123") */ public IdDt(String theResourceType, BigDecimal theIdPart) { this(theResourceType, toPlainStringWithNpeThrowIfNeeded(theIdPart)); } /** * Constructor * * @param theResourceType * The resource type (e.g. "Patient") * @param theIdPart * The ID (e.g. "123") */ public IdDt(String theResourceType, Long theIdPart) { this(theResourceType, toPlainStringWithNpeThrowIfNeeded(theIdPart)); } /** * Constructor * * @param theResourceType * The resource type (e.g. "Patient") * @param theId * The ID (e.g. "123") */ public IdDt(String theResourceType, String theId) { this(theResourceType, theId, null); } /** * Constructor * * @param theResourceType * The resource type (e.g. "Patient") * @param theId * The ID (e.g. "123") * @param theVersionId * The version ID ("e.g. "456") */ public IdDt(String theResourceType, String theId, String theVersionId) { this(null, theResourceType, theId, theVersionId); } /** * Constructor * * @param theBaseUrl * The server base URL (e.g. "http://example.com/fhir") * @param theResourceType * The resource type (e.g. "Patient") * @param theId * The ID (e.g. "123") * @param theVersionId * The version ID ("e.g. "456") */ public IdDt(String theBaseUrl, String theResourceType, String theId, String theVersionId) { myBaseUrl = theBaseUrl; myResourceType = theResourceType; myUnqualifiedId = theId; myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionId, null); myHaveComponentParts = true; if (isBlank(myBaseUrl) && isBlank(myResourceType) && isBlank(myUnqualifiedId) && isBlank(myUnqualifiedVersionId)) { myHaveComponentParts = false; } } /** * Creates an ID based on a given URL */ public IdDt(UriDt theUrl) { setValue(theUrl.getValueAsString()); } @Override public void applyTo(IBaseResource theResouce) { if (theResouce == null) { throw new NullPointerException("theResource can not be null"); } else if (theResouce instanceof IResource) { ((IResource) theResouce).setId(new IdDt(getValue())); } else if (theResouce instanceof IAnyResource) { ((IAnyResource) theResouce).setId(getValue()); } else { throw new IllegalArgumentException("Unknown resource class type, does not implement IResource or extend Resource"); } } /** * @deprecated Use {@link #getIdPartAsBigDecimal()} instead (this method was deprocated because its name is ambiguous) */ @Deprecated public BigDecimal asBigDecimal() { return getIdPartAsBigDecimal(); } @Override public boolean equals(Object theArg0) { if (!(theArg0 instanceof IdDt)) { return false; } IdDt id = (IdDt) theArg0; return StringUtils.equals(getValueAsString(), id.getValueAsString()); } /** * Returns true if this IdDt matches the given IdDt in terms of resource type and ID, but ignores the URL base */ @SuppressWarnings("deprecation") public boolean equalsIgnoreBase(IdDt theId) { if (theId == null) { return false; } if (theId.isEmpty()) { return isEmpty(); } return ObjectUtils.equals(getResourceType(), theId.getResourceType()) && ObjectUtils.equals(getIdPart(), theId.getIdPart()) && ObjectUtils.equals(getVersionIdPart(), theId.getVersionIdPart()); } /** * 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 * <code>http://example.com/fhir</code>. * <p> * This method may return null if the ID contains no base (e.g. "Patient/123") * </p> */ @Override public String getBaseUrl() { return myBaseUrl; } /** * 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". */ @Override public String getIdPart() { return myUnqualifiedId; } /** * Returns the unqualified portion of this ID as a big decimal, or <code>null</code> if the value is null * * @throws NumberFormatException * If the value is not a valid BigDecimal */ public BigDecimal getIdPartAsBigDecimal() { String val = getIdPart(); if (isBlank(val)) { return null; } return new BigDecimal(val); } /** * Returns the unqualified portion of this ID as a {@link Long}, or <code>null</code> if the value is null * * @throws NumberFormatException * If the value is not a valid Long */ @Override public Long getIdPartAsLong() { String val = getIdPart(); if (isBlank(val)) { return null; } return Long.parseLong(val); } @Override public String getResourceType() { return myResourceType; } /** * 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. * * @see #getIdPart() */ @Override public String getValue() { if (super.getValue() == null && myHaveComponentParts) { if (isLocal() || isUrn()) { return myUnqualifiedId; } StringBuilder b = new StringBuilder(); if (isNotBlank(myBaseUrl)) { b.append(myBaseUrl); if (myBaseUrl.charAt(myBaseUrl.length() - 1) != '/') { b.append('/'); } } if (isNotBlank(myResourceType)) { b.append(myResourceType); } if (b.length() > 0) { b.append('/'); } b.append(myUnqualifiedId); if (isNotBlank(myUnqualifiedVersionId)) { b.append('/'); b.append(Constants.PARAM_HISTORY); b.append('/'); b.append(myUnqualifiedVersionId); } String value = b.toString(); super.setValue(value); } return super.getValue(); } @Override public String getValueAsString() { return getValue(); } @Override public String getVersionIdPart() { return myUnqualifiedVersionId; } @Override public Long getVersionIdPartAsLong() { if (!hasVersionIdPart()) { return null; } return Long.parseLong(getVersionIdPart()); } /** * Returns true if this ID has a base url * * @see #getBaseUrl() */ @Override public boolean hasBaseUrl() { return isNotBlank(myBaseUrl); } @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); b.append(getValueAsString()); return b.toHashCode(); } @Override public boolean hasIdPart() { return isNotBlank(getIdPart()); } @Override public boolean hasResourceType() { return isNotBlank(myResourceType); } @Override public boolean hasVersionIdPart() { return isNotBlank(getVersionIdPart()); } /** * Returns <code>true</code> if this ID contains an absolute URL (in other words, a URL starting with "http://" or "https://" */ @Override public boolean isAbsolute() { if (StringUtils.isBlank(getValue())) { return false; } return UrlUtil.isAbsolute(getValue()); } @Override public boolean isEmpty() { return isBlank(getValue()); } @Override public boolean isIdPartValid() { String id = getIdPart(); if (StringUtils.isBlank(id)) { return false; } if (id.length() > 64) { return false; } for (int i = 0; i < id.length(); i++) { char nextChar = id.charAt(i); if (nextChar >= 'a' && nextChar <= 'z') { continue; } if (nextChar >= 'A' && nextChar <= 'Z') { continue; } if (nextChar >= '0' && nextChar <= '9') { continue; } if (nextChar == '-' || nextChar == '.') { continue; } return false; } return true; } @Override public boolean isIdPartValidLong() { return isValidLong(getIdPart()); } /** * Returns <code>true</code> if the ID is a local reference (in other words, * it begins with the '#' character) */ @Override public boolean isLocal() { return defaultString(myUnqualifiedId).startsWith("#"); } private boolean isUrn() { return defaultString(myUnqualifiedId).startsWith("urn:"); } @Override public boolean isVersionIdPartValidLong() { return isValidLong(getVersionIdPart()); } /** * 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. * @deprecated */ @Deprecated //override deprecated method @Override public void setId(IdDt theId) { setValue(theId.getValue()); } @Override public IIdType setParts(String theBaseUrl, String theResourceType, String theIdPart, String theVersionIdPart) { if (isNotBlank(theVersionIdPart)) { Validate.notBlank(theResourceType, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated"); Validate.notBlank(theIdPart, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated"); } if (isNotBlank(theBaseUrl) && isNotBlank(theIdPart)) { Validate.notBlank(theResourceType, "If theBaseUrl is populated and theIdPart is populated, theResourceType must be populated"); } setValue(null); myBaseUrl = theBaseUrl; myResourceType = theResourceType; myUnqualifiedId = theIdPart; myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionIdPart, null); myHaveComponentParts = true; return this; } /** * Set the value * * <p> * <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 * limit of 36 characters. * </p> * <p> * regex: [a-z0-9\-\.]{1,36} * </p> */ @Override public IdDt setValue(String theValue) throws DataFormatException { // TODO: add validation super.setValue(theValue); myHaveComponentParts = false; if (StringUtils.isBlank(theValue)) { myBaseUrl = null; super.setValue(null); myUnqualifiedId = null; myUnqualifiedVersionId = null; myResourceType = null; } else if (theValue.charAt(0) == '#' && theValue.length() > 1) { super.setValue(theValue); myBaseUrl = null; myUnqualifiedId = theValue; myUnqualifiedVersionId = null; myResourceType = null; myHaveComponentParts = true; } else if (theValue.startsWith("urn:")) { myBaseUrl = null; myUnqualifiedId = theValue; myUnqualifiedVersionId = null; myResourceType = null; myHaveComponentParts = true; } else { int vidIndex = theValue.indexOf("/_history/"); int idIndex; if (vidIndex != -1) { myUnqualifiedVersionId = theValue.substring(vidIndex + "/_history/".length()); idIndex = theValue.lastIndexOf('/', vidIndex - 1); myUnqualifiedId = theValue.substring(idIndex + 1, vidIndex); } else { idIndex = theValue.lastIndexOf('/'); myUnqualifiedId = theValue.substring(idIndex + 1); myUnqualifiedVersionId = null; } myBaseUrl = null; if (idIndex <= 0) { myResourceType = null; } else { int typeIndex = theValue.lastIndexOf('/', idIndex - 1); if (typeIndex == -1) { myResourceType = theValue.substring(0, idIndex); } else { myResourceType = theValue.substring(typeIndex + 1, idIndex); if (typeIndex > 4) { myBaseUrl = theValue.substring(0, typeIndex); } } } } return this; } /** * Set the value * * <p> * <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 * limit of 36 characters. * </p> * <p> * regex: [a-z0-9\-\.]{1,36} * </p> */ @Override public void setValueAsString(String theValue) throws DataFormatException { setValue(theValue); } @Override public String toString() { return getValue(); } /** * 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 * return a new IdDt containing ID "Patient/1". */ @Override public IdDt toUnqualified() { if (isLocal() || isUrn()) { return new IdDt(getValueAsString()); } return new IdDt(getResourceType(), getIdPart(), getVersionIdPart()); } @Override public IdDt toUnqualifiedVersionless() { if (isLocal() || isUrn()) { return new IdDt(getValueAsString()); } return new IdDt(getResourceType(), getIdPart()); } @Override public IdDt toVersionless() { if (isLocal() || isUrn()) { return new IdDt(getValueAsString()); } return new IdDt(getBaseUrl(), getResourceType(), getIdPart(), null); } @Override public IdDt withResourceType(String theResourceName) { if (isLocal() || isUrn()) { return new IdDt(getValueAsString()); } return new IdDt(theResourceName, getIdPart(), getVersionIdPart()); } /** * 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, * 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. * * @param theServerBase * The server base (e.g. "http://example.com/fhir") * @param theResourceType * The resource name (e.g. "Patient") * @return A fully qualified URL for this ID (e.g. "http://example.com/fhir/Patient/1") */ @Override public IdDt withServerBase(String theServerBase, String theResourceType) { if (isLocal() || isUrn()) { return new IdDt(getValueAsString()); } return new IdDt(theServerBase, theResourceType, getIdPart(), getVersionIdPart()); } /** * Creates a new instance of this ID which is identical, but refers to the specific version of this resource ID noted by theVersion. * * @param theVersion * The actual version string, e.g. "1" * @return A new instance of IdDt which is identical, but refers to the specific version of this resource ID noted by theVersion. */ @Override public IdDt withVersion(String theVersion) { Validate.notBlank(theVersion, "Version may not be null or empty"); if (isLocal() || isUrn()) { return new IdDt(getValueAsString()); } String existingValue = getValue(); int i = existingValue.indexOf(Constants.PARAM_HISTORY); String value; if (i > 1) { value = existingValue.substring(0, i - 1); } else { value = existingValue; } return new IdDt(value + '/' + Constants.PARAM_HISTORY + '/' + theVersion); } private static boolean isValidLong(String id) { if (StringUtils.isBlank(id)) { return false; } for (int i = 0; i < id.length(); i++) { if (Character.isDigit(id.charAt(i)) == false) { return false; } } return true; } /** * Construct a new ID with with form "urn:uuid:[UUID]" where [UUID] is a new, randomly * created UUID generated by {@link UUID#randomUUID()} */ public static IdDt newRandomUuid() { return new IdDt("urn:uuid:" + UUID.randomUUID().toString()); } /** * Retrieves the ID from the given resource instance */ public static IdDt of(IBaseResource theResouce) { if (theResouce == null) { throw new NullPointerException("theResource can not be null"); } IIdType retVal = theResouce.getIdElement(); if (retVal == null) { return null; } else if (retVal instanceof IdDt) { return (IdDt) retVal; } else { return new IdDt(retVal.getValue()); } } private static String toPlainStringWithNpeThrowIfNeeded(BigDecimal theIdPart) { if (theIdPart == null) { throw new NullPointerException("BigDecimal ID can not be null"); } return theIdPart.toPlainString(); } private static String toPlainStringWithNpeThrowIfNeeded(Long theIdPart) { if (theIdPart == null) { throw new NullPointerException("Long ID can not be null"); } return theIdPart.toString(); } }