View Javadoc
1   /*
2    * Copyright 2009 the original author or authors.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *     
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.openehealth.ipf.commons.ihe.xds.core.metadata;
17  
18  import lombok.AccessLevel;
19  import lombok.EqualsAndHashCode;
20  import lombok.Getter;
21  import org.apache.commons.beanutils.BeanUtils;
22  import org.apache.commons.lang3.StringUtils;
23  
24  import java.io.Serializable;
25  import java.util.*;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  /**
30   * Represents additional information about a patient, as an HL7v2 PID segment
31   * with the following specialities:
32   * <ul>
33   *     <li>Fields PID-3 (patient IDs), PID-5 (patient names), PID-7 (birth date), and
34   *     PID-8 (gender) can be manipulated both as HL7 strings and as XDS metadata objects.
35   *     <li>Fields PID-2, PID-4, PID-12 and PID-19 are prohibited by the XDS standard.</li>
36   * </ul>
37   * <b>IMPORTANT NOTE:</b> in-place modifications of the XDS metadata objects contained in the lists
38   * returned by {@link #getIds()}, {@link #getNames()}, and {@link #getAddresses()} will be <b>not</b>
39   * propagated to their HL7 string counterparts, therefore {@link ListIterator#set(Object)}
40   * shall be used to properly update the both representations.  For example,
41   * <pre>
42   * ListIterator&lt;Name&gt; iter = patientInfo.getNames();
43   * Name name = iter.next();
44   * name.setFamilyName("Krusenstern");
45   * iter.set(name);   // this statement is absolutely essential!
46   * </pre>
47   *
48   * @author Jens Riemschneider
49   * @author Dmytro Rud
50   */
51  @EqualsAndHashCode(doNotUseGetters = true)
52  public class PatientInfo implements Serializable {
53      private static final long serialVersionUID = 7202574584233259959L;
54  
55      /**
56       * Map from HL7 field ID to a list of field repetitions.
57       */
58      private final Map<String, List<String>> stringFields = new HashMap<>();
59  
60      /**
61       * A map covering a subset of fields with enhanced access mechanisms.
62       * The key is the field index (e.g. "PID-5"), and the value is
63       * <ul>
64       *     <li>for repeatable fields -- list of XDS metadata objects,</li>
65       *     <li>for non-repeatable fields -- always <code>null</code>.</li>
66       * </ul>
67       */
68      private final Map<String, List<? extends Hl7v2Based>> pojoFields;
69  
70      public PatientInfo() {
71          this.pojoFields = new HashMap<>();
72          this.pojoFields.put("PID-3", new ArrayList<Identifiable>());
73          this.pojoFields.put("PID-5", new ArrayList<Name>());
74          this.pojoFields.put("PID-7", null);
75          this.pojoFields.put("PID-8", null);
76          this.pojoFields.put("PID-11", new ArrayList<Address>());
77      }
78  
79      private List<String> getStrings(String fieldId) {
80          return stringFields.computeIfAbsent(fieldId, dummy -> new ArrayList<>());
81      }
82  
83      /**
84       * @return IDs of HL7 fields which are currently present in this SourcePatientInfo instance
85       *         (note: there can be present fields without any content)
86       */
87      public Set<String> getAllFieldIds() {
88          Set<String> result = new HashSet<>(stringFields.keySet());
89          return result;
90      }
91  
92      /**
93       * @return IDs of HL7 fields which are currently present in this SourcePatientInfo instance
94       *         and are not backed up by XDS metadata POJOs
95       *         (note: there can be present fields without any content)
96       */
97      public Set<String> getCustomFieldIds() {
98          Set<String> set = getAllFieldIds();
99          set.removeAll(pojoFields.keySet());
100         return set;
101     }
102 
103     /**
104      * @param fieldId HL7 field ID, e.g. "PID-3"
105      * @return iterator over a list of raw HL7 strings representing repetitions of the given HL7 field
106      */
107     public ListIterator<String> getHl7FieldIterator(String fieldId) {
108         ListIterator<String> stringsIterator = getStrings(fieldId).listIterator();
109         if (!pojoFields.containsKey(fieldId)) {
110             return stringsIterator;
111         }
112 
113         List<? extends Hl7v2Based> xdsFields = pojoFields.get(fieldId);
114         ListIterator xdsIterator = (xdsFields != null) ? xdsFields.listIterator() : null;
115 
116         return new SynchronizingListIterator<String, Hl7v2Based>(stringsIterator, xdsIterator) {
117             private void validateParameter(String s) {
118                 if ((s != null) && s.contains("~")) {
119                     throw new RuntimeException("Repetitions shall be handled by multiple calls to .add()/.set(), and not by the tilde in " + s);
120                 }
121             }
122 
123             @Override
124             public void set(String s) {
125                 validateParameter(s);
126                 getIterator().set(s);
127                 switch (fieldId) {
128                     case "PID-3":
129                         getOtherIterator().set(Hl7v2Based.parse(s, Identifiable.class));
130                         break;
131                     case "PID-5":
132                         getOtherIterator().set(Hl7v2Based.parse(s, XpnName.class));
133                         break;
134                     case "PID-7":
135                         break;
136                     case "PID-8":
137                         break;
138                     case "PID-11":
139                         getOtherIterator().set(Hl7v2Based.parse(s, Address.class));
140                         break;
141                     default:
142                         throw new RuntimeException("This line shall be not reachable, please report a bug");
143                 }
144             }
145 
146             @Override
147             public void add(String s) {
148                 validateParameter(s);
149                 getIterator().add(s);
150                 switch (fieldId) {
151                     case "PID-3":
152                         getOtherIterator().add(Hl7v2Based.parse(s, Identifiable.class));
153                         break;
154                     case "PID-5":
155                         getOtherIterator().add(Hl7v2Based.parse(s, XpnName.class));
156                         break;
157                     case "PID-7":
158                         break;
159                     case "PID-8":
160                         break;
161                     case "PID-11":
162                         getOtherIterator().add(Hl7v2Based.parse(s, Address.class));
163                         break;
164                     default:
165                         throw new RuntimeException("This line shall be not reachable, please report a bug");
166                 }
167             }
168         };
169     }
170 
171     private <T extends Hl7v2Based> ListIterator<T> getXdsFieldIterator(String fieldId) {
172         if (pojoFields.get(fieldId) == null) {
173             throw new IllegalArgumentException(fieldId + " is not a known repeatable SourcePatientInfo element");
174         }
175 
176         ListIterator<T> xdsIterator = (ListIterator<T>) pojoFields.get(fieldId).listIterator();
177         ListIterator<String> stringsIterator = getStrings(fieldId).listIterator();
178 
179         return new SynchronizingListIterator<T, String>(xdsIterator, stringsIterator) {
180             private T prepareValue(T xdsObject) {
181                 if ("PID-5".equals(fieldId) && (xdsObject != null) && !(xdsObject instanceof XpnName)) {
182                     Name xpnName = new XpnName();
183                     try {
184                         BeanUtils.copyProperties(xpnName, xdsObject);
185                         return (T) xpnName;
186                     } catch (Exception e) {
187                         throw new RuntimeException("Could not copy properties", e);
188                     }
189                 }
190                 return xdsObject;
191             }
192 
193             @Override
194             public void set(T xdsObject) {
195                 xdsObject = prepareValue(xdsObject);
196                 getOtherIterator().set(StringUtils.trimToEmpty(Hl7v2Based.render(xdsObject)));
197                 getIterator().set(xdsObject);
198             }
199 
200             @Override
201             public void add(T xdsObject) {
202                 xdsObject = prepareValue(xdsObject);
203                 getOtherIterator().add(StringUtils.trimToEmpty(Hl7v2Based.render(xdsObject)));
204                 getIterator().add(xdsObject);
205             }
206         };
207     }
208 
209     /**
210      * Returns a snapshot of list of patient IDs.  See the note in the class javadoc.
211      * @return iterator over the IDs of the patient (PID-3).
212      */
213     public ListIterator<Identifiable> getIds() {
214         return getXdsFieldIterator("PID-3");
215     }
216 
217     /**
218      * Returns a snapshot of the list of patient names.  See the note in the class javadoc.
219      * @return iterator over the names of the patient (PID-5).
220      */
221     public ListIterator<Name> getNames() {
222         return getXdsFieldIterator("PID-5");
223     }
224 
225     private String getFirstStringValue(String fieldName) {
226         List<String> list = stringFields.get(fieldName);
227         return ((list != null) && !list.isEmpty()) ? list.get(0) : null;
228     }
229 
230     /**
231      * @return the date of birth of the patient (PID-7).
232      */
233     public Timestamp getDateOfBirth() {
234         String s = getFirstStringValue("PID-7");
235         if (s != null) {
236             int pos = s.indexOf('^');
237             return Timestamp.fromHL7((pos > 0) ? s.substring(0, pos) : s);
238         }
239         return null;
240     }
241 
242     /**
243      * @param date the date of birth of the patient (PID-7).
244      */
245     public void setDateOfBirth(Timestamp date) {
246         setDateOfBirthString(Timestamp.toHL7(date));
247     }
248 
249     /**
250      * @param date the date of birth of the patient (PID-7).
251      */
252     public void setDateOfBirth(String date) {
253         setDateOfBirthString(StringUtils.stripToNull(date));
254     }
255 
256     private void setDateOfBirthString(String date) {
257         List<String> strings = getStrings("PID-7");
258         strings.clear();
259         if (date != null) {
260             strings.add(date);
261         }
262     }
263 
264     /**
265      * @return the gender of the patient (PID-8).
266      */
267     public String getGender() {
268         return StringUtils.stripToNull(getFirstStringValue("PID-8"));
269     }
270 
271     /**
272      * @param gender the gender of the patient (PID-8).
273      */
274     public void setGender(String gender) {
275         List<String> strings = getStrings("PID-8");
276         strings.clear();
277         String s = StringUtils.stripToNull(gender);
278         if (s != null) {
279             strings.add(s);
280         }
281     }
282 
283     /**
284      * Returns a snapshot of the list of patient addresses.  See the note in the class javadoc.
285      * @return iterator over thr addresses of the patient (PID-11).
286      */
287     public ListIterator<Address> getAddresses() {
288         return getXdsFieldIterator("PID-11");
289     }
290 
291     abstract private static class SynchronizingListIterator<T, OtherT> implements ListIterator<T> {
292         @Getter(AccessLevel.PROTECTED) private final ListIterator<T> iterator;
293         @Getter(AccessLevel.PROTECTED) private final ListIterator<OtherT> otherIterator;
294 
295         SynchronizingListIterator(ListIterator<T> iterator, ListIterator<OtherT> otherIterator) {
296             this.iterator = iterator;
297             this.otherIterator = otherIterator;
298         }
299 
300         @Override
301         public boolean hasNext() {
302             return iterator.hasNext();
303         }
304 
305         @Override
306         public T next() {
307             if (otherIterator != null) {
308                 otherIterator.next();
309             }
310             return iterator.next();
311         }
312 
313         @Override
314         public boolean hasPrevious() {
315             return iterator.hasPrevious();
316         }
317 
318         @Override
319         public T previous() {
320             if (otherIterator != null) {
321                 otherIterator.previous();
322             }
323             return iterator.previous();
324         }
325 
326         @Override
327         public int nextIndex() {
328             return iterator.nextIndex();
329         }
330 
331         @Override
332         public int previousIndex() {
333             return iterator.previousIndex();
334         }
335 
336         @Override
337         public void remove() {
338             if (otherIterator != null) {
339                 otherIterator.remove();
340             }
341             iterator.remove();
342         }
343     }
344 
345     public static class Hl7FieldIdComparator implements Comparator<String> {
346         public static final Pattern FIELD_ID_PATTERN = Pattern.compile("([A-Z][A-Z][A-Z0-9])-(\\d\\d?)");
347 
348         @Override
349         public int compare(String o1, String o2) {
350             Matcher matcher1 = FIELD_ID_PATTERN.matcher(o1);
351             Matcher matcher2 = FIELD_ID_PATTERN.matcher(o2);
352             if (matcher1.matches() && matcher2.matches() && matcher1.group(1).equals(matcher2.group(1))) {
353                 return Integer.parseInt(matcher1.group(2)) - Integer.parseInt(matcher2.group(2));
354             }
355             return o1.compareTo(o2);
356         }
357     }
358 
359     @Override
360     public String toString() {
361         StringBuilder sb = new StringBuilder()
362                 .append("PatientInfo(")
363                 .append("ids=")
364                 .append(pojoFields.get("PID-3"))
365                 .append(", names=")
366                 .append(pojoFields.get("PID-5"))
367                 .append(", birthDate=")
368                 .append(getDateOfBirth())
369                 .append(", gender=")
370                 .append(getGender())
371                 .append(", addresses=")
372                 .append(pojoFields.get("PID-11"));
373 
374         getCustomFieldIds().stream().sorted(new Hl7FieldIdComparator()).forEach(fieldId -> {
375             List<String> values = stringFields.get(fieldId);
376             if (!values.isEmpty()) {
377                 sb.append(", ").append(fieldId).append('=').append(values);
378             }
379         });
380 
381         return sb.append(')').toString();
382     }
383 }