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