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