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