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