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