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