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