001package ca.uhn.fhir.model.primitive;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022import static org.apache.commons.lang3.StringUtils.isBlank;
023import static org.apache.commons.lang3.StringUtils.isNotBlank;
024
025import java.math.BigDecimal;
026import java.util.UUID;
027
028import org.apache.commons.lang3.ObjectUtils;
029import org.apache.commons.lang3.StringUtils;
030import org.apache.commons.lang3.Validate;
031import org.apache.commons.lang3.builder.HashCodeBuilder;
032import org.hl7.fhir.instance.model.api.IAnyResource;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.instance.model.api.IIdType;
035
036import ca.uhn.fhir.model.api.IPrimitiveDatatype;
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-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        }
174
175        /**
176         * Creates an ID based on a given URL
177         */
178        public IdDt(UriDt theUrl) {
179                setValue(theUrl.getValueAsString());
180        }
181
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        private String determineLocalPrefix(String theValue) {
203                if (theValue == null || theValue.isEmpty()) {
204                        return null;
205                }
206                if (theValue.startsWith("#")) {
207                        return "#";
208                }
209                int lastPrefix = -1;
210                for (int i = 0; i < theValue.length(); i++) {
211                        char nextChar = theValue.charAt(i);
212                        if (nextChar == ':') {
213                                lastPrefix = i;
214                        } else if (!Character.isLetter(nextChar) || !Character.isLowerCase(nextChar)) {
215                                break;
216                        }
217                }
218                if (lastPrefix != -1) {
219                        String candidate = theValue.substring(0, lastPrefix + 1);
220                        if (candidate.startsWith("cid:") || candidate.startsWith("urn:")) {
221                                return candidate;
222                        } else {
223                                return null;
224                        }
225                } else {
226                        return null;
227                }
228        }
229
230        @Override
231        public boolean equals(Object theArg0) {
232                if (!(theArg0 instanceof IdDt)) {
233                        return false;
234                }
235                IdDt id = (IdDt) theArg0;
236                return StringUtils.equals(getValueAsString(), id.getValueAsString());
237        }
238
239        /**
240         * Returns true if this IdDt matches the given IdDt in terms of resource type and ID, but ignores the URL base
241         */
242        @SuppressWarnings("deprecation")
243        public boolean equalsIgnoreBase(IdDt theId) {
244                if (theId == null) {
245                        return false;
246                }
247                if (theId.isEmpty()) {
248                        return isEmpty();
249                }
250                return ObjectUtils.equals(getResourceType(), theId.getResourceType()) && ObjectUtils.equals(getIdPart(), theId.getIdPart()) && ObjectUtils.equals(getVersionIdPart(), theId.getVersionIdPart());
251        }
252
253        /**
254         * 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
255         * <code>http://example.com/fhir</code>.
256         * <p>
257         * This method may return null if the ID contains no base (e.g. "Patient/123")
258         * </p>
259         */
260        @Override
261        public String getBaseUrl() {
262                return myBaseUrl;
263        }
264
265        /**
266         * 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".
267         */
268        @Override
269        public String getIdPart() {
270                return myUnqualifiedId;
271        }
272
273        /**
274         * Returns the unqualified portion of this ID as a big decimal, or <code>null</code> if the value is null
275         * 
276         * @throws NumberFormatException
277         *             If the value is not a valid BigDecimal
278         */
279        public BigDecimal getIdPartAsBigDecimal() {
280                String val = getIdPart();
281                if (isBlank(val)) {
282                        return null;
283                }
284                return new BigDecimal(val);
285        }
286
287        /**
288         * Returns the unqualified portion of this ID as a {@link Long}, or <code>null</code> if the value is null
289         * 
290         * @throws NumberFormatException
291         *             If the value is not a valid Long
292         */
293        @Override
294        public Long getIdPartAsLong() {
295                String val = getIdPart();
296                if (isBlank(val)) {
297                        return null;
298                }
299                return Long.parseLong(val);
300        }
301
302        @Override
303        public String getResourceType() {
304                return myResourceType;
305        }
306
307        /**
308         * 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.
309         * 
310         * @see #getIdPart()
311         */
312        @Override
313        public String getValue() {
314                if (super.getValue() == null && myHaveComponentParts) {
315                        
316                        if (determineLocalPrefix(myBaseUrl) != null && myResourceType == null && myUnqualifiedVersionId == null) {
317                                return myBaseUrl + myUnqualifiedId;
318                        }
319                        
320                        StringBuilder b = new StringBuilder();
321                        if (isNotBlank(myBaseUrl)) {
322                                b.append(myBaseUrl);
323                                if (myBaseUrl.charAt(myBaseUrl.length() - 1) != '/') {
324                                        b.append('/');
325                                }
326                        }
327
328                        if (isNotBlank(myResourceType)) {
329                                b.append(myResourceType);
330                        }
331
332                        if (b.length() > 0) {
333                                b.append('/');
334                        }
335
336                        b.append(myUnqualifiedId);
337                        if (isNotBlank(myUnqualifiedVersionId)) {
338                                b.append('/');
339                                b.append(Constants.PARAM_HISTORY);
340                                b.append('/');
341                                b.append(myUnqualifiedVersionId);
342                        }
343                        String value = b.toString();
344                        super.setValue(value);
345                }
346                return super.getValue();
347        }
348
349        @Override
350        public String getValueAsString() {
351                return getValue();
352        }
353
354        @Override
355        public String getVersionIdPart() {
356                return myUnqualifiedVersionId;
357        }
358
359        public Long getVersionIdPartAsLong() {
360                if (!hasVersionIdPart()) {
361                        return null;
362                } else {
363                        return Long.parseLong(getVersionIdPart());
364                }
365        }
366
367        /**
368         * Returns true if this ID has a base url
369         * 
370         * @see #getBaseUrl()
371         */
372        public boolean hasBaseUrl() {
373                return isNotBlank(myBaseUrl);
374        }
375
376        @Override
377        public int hashCode() {
378                HashCodeBuilder b = new HashCodeBuilder();
379                b.append(getValueAsString());
380                return b.toHashCode();
381        }
382
383        @Override
384        public boolean hasIdPart() {
385                return isNotBlank(getIdPart());
386        }
387
388        @Override
389        public boolean hasResourceType() {
390                return isNotBlank(myResourceType);
391        }
392
393        @Override
394        public boolean hasVersionIdPart() {
395                return isNotBlank(getVersionIdPart());
396        }
397
398        /**
399         * Returns <code>true</code> if this ID contains an absolute URL (in other words, a URL starting with "http://" or "https://"
400         */
401        @Override
402        public boolean isAbsolute() {
403                if (StringUtils.isBlank(getValue())) {
404                        return false;
405                }
406                return UrlUtil.isAbsolute(getValue());
407        }
408
409        @Override
410        public boolean isEmpty() {
411                return isBlank(getValue());
412        }
413
414        @Override
415        public boolean isIdPartValid() {
416                String id = getIdPart();
417                if (StringUtils.isBlank(id)) {
418                        return false;
419                }
420                if (id.length() > 64) {
421                        return false;
422                }
423                for (int i = 0; i < id.length(); i++) {
424                        char nextChar = id.charAt(i);
425                        if (nextChar >= 'a' && nextChar <= 'z') {
426                                continue;
427                        }
428                        if (nextChar >= 'A' && nextChar <= 'Z') {
429                                continue;
430                        }
431                        if (nextChar >= '0' && nextChar <= '9') {
432                                continue;
433                        }
434                        if (nextChar == '-' || nextChar == '.') {
435                                continue;
436                        }
437                        return false;
438                }
439                return true;
440        }
441
442        @Override
443        public boolean isIdPartValidLong() {
444                return isValidLong(getIdPart());
445        }
446
447        /**
448         * Returns <code>true</code> if the ID is a local reference (in other words, it begins with the '#' character)
449         */
450        @Override
451        public boolean isLocal() {
452                return "#".equals(myBaseUrl);
453        }
454
455        @Override
456        public boolean isVersionIdPartValidLong() {
457                return isValidLong(getVersionIdPart());
458        }
459        
460        /**
461         * 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.
462         */
463        @Override
464        public void setId(IdDt theId) {
465                setValue(theId.getValue());
466        }
467
468        /**
469         * Set the value
470         * 
471         * <p>
472         * <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
473         * limit of 36 characters.
474         * </p>
475         * <p>
476         * regex: [a-z0-9\-\.]{1,36}
477         * </p>
478         */
479        @Override
480        public IdDt setValue(String theValue) throws DataFormatException {
481                // TODO: add validation
482                super.setValue(theValue);
483                myHaveComponentParts = false;
484                
485                String localPrefix = determineLocalPrefix(theValue);
486                
487                if (StringUtils.isBlank(theValue)) {
488                        myBaseUrl = null;
489                        super.setValue(null);
490                        myUnqualifiedId = null;
491                        myUnqualifiedVersionId = null;
492                        myResourceType = null;
493                } else if (theValue.charAt(0) == '#' && theValue.length() > 1) {
494                        super.setValue(theValue);
495                        myBaseUrl = "#";
496                        myUnqualifiedId = theValue.substring(1);
497                        myUnqualifiedVersionId = null;
498                        myResourceType = null;
499                        myHaveComponentParts = true;
500                } else if (localPrefix != null) {
501                        myBaseUrl = localPrefix;
502                        myUnqualifiedId = theValue.substring(localPrefix.length());
503                        myUnqualifiedVersionId = null;
504                        myResourceType = null;
505                        myHaveComponentParts = true;
506                } else {
507                        int vidIndex = theValue.indexOf("/_history/");
508                        int idIndex;
509                        if (vidIndex != -1) {
510                                myUnqualifiedVersionId = theValue.substring(vidIndex + "/_history/".length());
511                                idIndex = theValue.lastIndexOf('/', vidIndex - 1);
512                                myUnqualifiedId = theValue.substring(idIndex + 1, vidIndex);
513                        } else {
514                                idIndex = theValue.lastIndexOf('/');
515                                myUnqualifiedId = theValue.substring(idIndex + 1);
516                                myUnqualifiedVersionId = null;
517                        }
518
519                        myBaseUrl = null;
520                        if (idIndex <= 0) {
521                                myResourceType = null;
522                        } else {
523                                int typeIndex = theValue.lastIndexOf('/', idIndex - 1);
524                                if (typeIndex == -1) {
525                                        myResourceType = theValue.substring(0, idIndex);
526                                } else {
527                                        myResourceType = theValue.substring(typeIndex + 1, idIndex);
528
529                                        if (typeIndex > 4) {
530                                                myBaseUrl = theValue.substring(0, typeIndex);
531                                        }
532
533                                }
534                        }
535
536                }
537                return this;
538        }
539
540        /**
541         * Set the value
542         * 
543         * <p>
544         * <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
545         * limit of 36 characters.
546         * </p>
547         * <p>
548         * regex: [a-z0-9\-\.]{1,36}
549         * </p>
550         */
551        @Override
552        public void setValueAsString(String theValue) throws DataFormatException {
553                setValue(theValue);
554        }
555
556        @Override
557        public String toString() {
558                return getValue();
559        }
560
561        /**
562         * 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
563         * return a new IdDt containing ID "Patient/1".
564         */
565        @Override
566        public IdDt toUnqualified() {
567                return new IdDt(getResourceType(), getIdPart(), getVersionIdPart());
568        }
569
570        @Override
571        public IdDt toUnqualifiedVersionless() {
572                if (isLocal()) {
573                        return toVersionless();
574                }
575                return new IdDt(getResourceType(), getIdPart());
576        }
577
578        @Override
579        public IdDt toVersionless() {
580                return new IdDt(getBaseUrl(), getResourceType(), getIdPart(), null);
581        }
582
583        @Override
584        public IdDt withResourceType(String theResourceName) {
585                return new IdDt(theResourceName, getIdPart(), getVersionIdPart());
586        }
587
588        /**
589         * 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,
590         * 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.
591         * 
592         * @param theServerBase
593         *            The server base (e.g. "http://example.com/fhir")
594         * @param theResourceType
595         *            The resource name (e.g. "Patient")
596         * @return A fully qualified URL for this ID (e.g. "http://example.com/fhir/Patient/1")
597         */
598        @Override
599        public IdDt withServerBase(String theServerBase, String theResourceType) {
600                return new IdDt(theServerBase, theResourceType, getIdPart(), getVersionIdPart());
601        }
602
603        /**
604         * Creates a new instance of this ID which is identical, but refers to the specific version of this resource ID noted by theVersion.
605         * 
606         * @param theVersion
607         *            The actual version string, e.g. "1"
608         * @return A new instance of IdDt which is identical, but refers to the specific version of this resource ID noted by theVersion.
609         */
610        public IdDt withVersion(String theVersion) {
611                Validate.notBlank(theVersion, "Version may not be null or empty");
612
613                String existingValue = getValue();
614
615                int i = existingValue.indexOf(Constants.PARAM_HISTORY);
616                String value;
617                if (i > 1) {
618                        value = existingValue.substring(0, i - 1);
619                } else {
620                        value = existingValue;
621                }
622
623                return new IdDt(value + '/' + Constants.PARAM_HISTORY + '/' + theVersion);
624        }
625
626        private static boolean isValidLong(String id) {
627                if (StringUtils.isBlank(id)) {
628                        return false;
629                }
630                for (int i = 0; i < id.length(); i++) {
631                        if (Character.isDigit(id.charAt(i)) == false) {
632                                return false;
633                        }
634                }
635                return true;
636        }
637
638        /**
639         * Construct a new ID with with form "urn:uuid:[UUID]" where [UUID] is a new, randomly
640         * created UUID generated by {@link UUID#randomUUID()}
641         */
642        public static IdDt newRandomUuid() {
643                return new IdDt("urn:uuid:" + UUID.randomUUID().toString());
644        }
645
646        /**
647         * Retrieves the ID from the given resource instance
648         */
649        public static IdDt of(IBaseResource theResouce) {
650                if (theResouce == null) {
651                        throw new NullPointerException("theResource can not be null");
652                } else {
653                        IIdType retVal = theResouce.getIdElement();
654                        if (retVal == null) {
655                                return null;
656                        } else if (retVal instanceof IdDt) {
657                                return (IdDt) retVal;
658                        } else {
659                                return new IdDt(retVal.getValue());
660                        }
661                }
662        }
663
664        private static String toPlainStringWithNpeThrowIfNeeded(BigDecimal theIdPart) {
665                if (theIdPart == null) {
666                        throw new NullPointerException("BigDecimal ID can not be null");
667                }
668                return theIdPart.toPlainString();
669        }
670
671        private static String toPlainStringWithNpeThrowIfNeeded(Long theIdPart) {
672                if (theIdPart == null) {
673                        throw new NullPointerException("Long ID can not be null");
674                }
675                return theIdPart.toString();
676        }
677
678        @Override
679        public IIdType setParts(String theBaseUrl, String theResourceType, String theIdPart, String theVersionIdPart) {
680                if (isNotBlank(theVersionIdPart)) {
681                        Validate.notBlank(theResourceType, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated");
682                        Validate.notBlank(theIdPart, "If theVersionIdPart is populated, theResourceType and theIdPart must be populated");
683                }
684                if (isNotBlank(theBaseUrl) && isNotBlank(theIdPart)) {
685                        Validate.notBlank(theResourceType, "If theBaseUrl is populated and theIdPart is populated, theResourceType must be populated");
686                }
687                
688                setValue(null);
689                
690                myBaseUrl = theBaseUrl;
691                myResourceType = theResourceType;
692                myUnqualifiedId = theIdPart;
693                myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionIdPart, null);
694                myHaveComponentParts = true;
695                
696                return this;
697        }
698
699}