• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 29867170267

21 Jul 2026 05:53PM UTC coverage: 64.101% (+0.1%) from 63.994%
29867170267

push

github

ibacher
Require Get Patient Programs privilege for by-uuid program lookups (#6318)

* Require Get Patient Programs privilege for by-uuid program lookups

ProgramWorkflowService.getPatientProgramByUuid and getPatientStateByUuid
had no @Authorized annotation. Because AuthorizationAdvice only enforces
when a method declares a privilege, both fell through with no check at
all, letting any authenticated user read program-enrollment PHI by uuid.
Every other read on the service, including getPatientProgram(Integer),
already requires Get Patient Programs; annotate both by-uuid methods to
match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Require Get Patient Programs privilege for patient program attribute lookup

getPatientProgramAttributeByAttributeName returns patient program
attribute values keyed by patient id, yet lacked an @Authorized check.
Its two sibling attribute readers (getPatientProgramAttributeByUuid and
getPatientProgramByAttributeNameAndValue) already guard on Get Patient
Programs, so this was a missed PHI read of the same class addressed by
GHSA-gqf9-38mg-wgqg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpKNfxm3QjwUBDePEtFm2H

* Add authorization tests pinning the by-uuid program privilege checks

The existing tests for getPatientProgramByUuid, getPatientStateByUuid and
getPatientProgramAttributeByAttributeName run as the privileged superuser
and assert only find/null behavior, so they pass even if the
@Authorized(Get Patient Programs) checks added by this branch are removed.
getPatientProgramAttributeByAttributeName had no test at all.

Add a test per method that logs out (dropping Get Patient Programs) and
asserts APIAuthenticationException is thrown for otherwise-valid arguments,
mirroring AuthorizationAdviceTest and AdministrationServiceTest. These fail
if any of the three annotations is removed, pinning the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cl... (continued)

24273 of 37867 relevant lines covered (64.1%)

0.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.13
/api/src/main/java/org/openmrs/api/impl/PatientServiceImpl.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.api.impl;
11

12
import java.util.ArrayList;
13
import java.util.Collection;
14
import java.util.Collections;
15
import java.util.Date;
16
import java.util.HashSet;
17
import java.util.LinkedHashMap;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Set;
21
import java.util.UUID;
22
import java.util.stream.Collectors;
23

24
import org.apache.commons.lang3.StringUtils;
25
import org.openmrs.Allergen;
26
import org.openmrs.Allergies;
27
import org.openmrs.Allergy;
28
import org.openmrs.BaseOpenmrsMetadata;
29
import org.openmrs.Concept;
30
import org.openmrs.Condition;
31
import org.openmrs.Encounter;
32
import org.openmrs.Location;
33
import org.openmrs.MedicationDispense;
34
import org.openmrs.Obs;
35
import org.openmrs.Order;
36
import org.openmrs.Patient;
37
import org.openmrs.PatientIdentifier;
38
import org.openmrs.PatientIdentifierType;
39
import org.openmrs.PatientProgram;
40
import org.openmrs.Person;
41
import org.openmrs.PersonAddress;
42
import org.openmrs.PersonAttribute;
43
import org.openmrs.PersonName;
44
import org.openmrs.Relationship;
45
import org.openmrs.User;
46
import org.openmrs.Visit;
47
import org.openmrs.api.APIException;
48
import org.openmrs.api.BlankIdentifierException;
49
import org.openmrs.api.ConditionService;
50
import org.openmrs.api.DuplicateIdentifierException;
51
import org.openmrs.api.EncounterService;
52
import org.openmrs.api.InsufficientIdentifiersException;
53
import org.openmrs.api.MedicationDispenseService;
54
import org.openmrs.api.MissingRequiredIdentifierException;
55
import org.openmrs.api.ObsService;
56
import org.openmrs.api.PatientIdentifierException;
57
import org.openmrs.api.PatientIdentifierTypeLockedException;
58
import org.openmrs.api.PatientService;
59
import org.openmrs.api.PersonService;
60
import org.openmrs.api.ProgramWorkflowService;
61
import org.openmrs.api.UserService;
62
import org.openmrs.api.VisitService;
63
import org.openmrs.api.context.Context;
64
import org.openmrs.api.db.PatientDAO;
65
import org.openmrs.api.db.hibernate.HibernateUtil;
66
import org.openmrs.parameter.EncounterSearchCriteria;
67
import org.openmrs.parameter.EncounterSearchCriteriaBuilder;
68
import org.openmrs.parameter.MedicationDispenseCriteria;
69
import org.openmrs.parameter.MedicationDispenseCriteriaBuilder;
70
import org.openmrs.patient.IdentifierValidator;
71
import org.openmrs.patient.impl.LuhnIdentifierValidator;
72
import org.openmrs.person.PersonMergeLog;
73
import org.openmrs.person.PersonMergeLogData;
74
import org.openmrs.serialization.SerializationException;
75
import org.openmrs.util.OpenmrsConstants;
76
import org.openmrs.util.OpenmrsUtil;
77
import org.openmrs.util.PrivilegeConstants;
78
import org.openmrs.validator.PatientIdentifierValidator;
79
import org.slf4j.Logger;
80
import org.slf4j.LoggerFactory;
81
import org.springframework.transaction.annotation.Transactional;
82

83
/**
84
 * Default implementation of the patient service. This class should not be used on its own. The
85
 * current OpenMRS implementation should be fetched from the Context via
86
 * <code>Context.getPatientService()</code>
87
 * 
88
 * @see org.openmrs.api.context.Context
89
 * @see org.openmrs.api.PatientService
90
 * @see org.openmrs.api.PersonService
91
 */
92
@Transactional
93
public class PatientServiceImpl extends BaseOpenmrsService implements PatientService {
1✔
94
        
95
        private static final Logger log = LoggerFactory.getLogger(PatientServiceImpl.class);
1✔
96
        
97
        private PatientDAO dao;
98
        
99
        /**
100
         * PatientIdentifierValidators registered through spring's applicationContext-service.xml
101
         */
102
        private static Map<Class<? extends IdentifierValidator>, IdentifierValidator> identifierValidators = null;
1✔
103
        
104
        /**
105
         * @see org.openmrs.api.PatientService#setPatientDAO(org.openmrs.api.db.PatientDAO)
106
         */
107
        @Override
108
        public void setPatientDAO(PatientDAO dao) {
109
                this.dao = dao;
1✔
110
        }
1✔
111
        
112
        /**
113
         * Clean up after this class. Set the static var to null so that the classloader can reclaim the
114
         * space.
115
         * 
116
         * @see org.openmrs.api.impl.BaseOpenmrsService#onShutdown()
117
         */
118
        @Override
119
        public void onShutdown() {
120
                setIdentifierValidators(null);
×
121
        }
×
122
        
123
        /**
124
         * @see org.openmrs.api.PatientService#savePatient(org.openmrs.Patient)
125
         */
126
        @Override
127
        public Patient savePatient(Patient patient) throws APIException {
128
                requireAppropriatePatientModificationPrivilege(patient);
1✔
129

130
                if (!patient.getVoided() && patient.getIdentifiers().size() == 1) {
1✔
131
                        patient.getPatientIdentifier().setPreferred(true);
1✔
132
                }
133

134
                if (!patient.getVoided()) {
1✔
135
                        checkPatientIdentifiers(patient);
1✔
136
                }
137

138
                setPreferredPatientIdentifier(patient);
1✔
139
                setPreferredPatientName(patient);
1✔
140
                setPreferredPatientAddress(patient);
1✔
141

142
                return dao.savePatient(patient);
1✔
143
        }
144

145
        private void requireAppropriatePatientModificationPrivilege(Patient patient) {
146
                if (patient.getPatientId() == null) {
1✔
147
                        Context.requirePrivilege(PrivilegeConstants.ADD_PATIENTS);
1✔
148
                } else {
149
                        Context.requirePrivilege(PrivilegeConstants.EDIT_PATIENTS);
1✔
150
                }
151
                if (patient.getVoided()) {
1✔
152
                        Context.requirePrivilege(PrivilegeConstants.DELETE_PATIENTS);
1✔
153
                }
154
        }
1✔
155

156
        private void setPreferredPatientIdentifier(Patient patient) {
157
                PatientIdentifier preferredIdentifier = null;
1✔
158
                PatientIdentifier possiblePreferredId = patient.getPatientIdentifier();
1✔
159
                if (possiblePreferredId != null && possiblePreferredId.getPreferred() && !possiblePreferredId.getVoided()) {
1✔
160
                        preferredIdentifier = possiblePreferredId;
1✔
161
                }
162

163
                for (PatientIdentifier id : patient.getIdentifiers()) {
1✔
164
                        if (preferredIdentifier == null && !id.getVoided()) {
1✔
165
                                id.setPreferred(true);
1✔
166
                                preferredIdentifier = id;
1✔
167
                                continue;
1✔
168
                        }
169

170
                        if (!id.equals(preferredIdentifier)) {
1✔
171
                                id.setPreferred(false);
1✔
172
                        }
173
                }
1✔
174
        }
1✔
175

176
        private void setPreferredPatientName(Patient patient) {
177
                PersonName preferredName = null;
1✔
178
                PersonName possiblePreferredName = patient.getPersonName();
1✔
179
                if (possiblePreferredName != null && possiblePreferredName.getPreferred() && !possiblePreferredName.getVoided()) {
1✔
180
                        preferredName = possiblePreferredName;
1✔
181
                }
182

183
                for (PersonName name : patient.getNames()) {
1✔
184
                        if (preferredName == null && !name.getVoided()) {
1✔
185
                                name.setPreferred(true);
1✔
186
                                preferredName = name;
1✔
187
                                continue;
1✔
188
                        }
189

190
                        if (!name.equals(preferredName)) {
1✔
191
                                name.setPreferred(false);
1✔
192
                        }
193
                }
1✔
194
        }
1✔
195
        
196
        private void setPreferredPatientAddress(Patient patient) {
197
                PersonAddress preferredAddress = null;
1✔
198
                PersonAddress possiblePreferredAddress = patient.getPersonAddress();
1✔
199
                if (possiblePreferredAddress != null && possiblePreferredAddress.getPreferred()
1✔
200
                                && !possiblePreferredAddress.getVoided()) {
1✔
201
                        preferredAddress = possiblePreferredAddress;
1✔
202
                }
203

204
                for (PersonAddress address : patient.getAddresses()) {
1✔
205
                        if (preferredAddress == null && !address.getVoided()) {
1✔
206
                                address.setPreferred(true);
1✔
207
                                preferredAddress = address;
1✔
208
                                continue;
1✔
209
                        }
210

211
                        if (!address.equals(preferredAddress)) {
1✔
212
                                address.setPreferred(false);
1✔
213
                        }
214
                }
1✔
215
        }
1✔
216
        
217
        /**
218
         * @see org.openmrs.api.PatientService#getPatient(java.lang.Integer)
219
         */
220
        @Override
221
        @Transactional(readOnly = true)
222
        public Patient getPatient(Integer patientId) throws APIException {
223
                return dao.getPatient(patientId);
1✔
224
        }
225
        
226
        @Override
227
        @Transactional(readOnly = true)
228
        public Patient getPatientOrPromotePerson(Integer patientOrPersonId) {
229
                Person person = Context.getPersonService().getPerson(patientOrPersonId);
1✔
230
                if (person == null) {
1✔
231
                        return null;
1✔
232
                }
233
                person = HibernateUtil.getRealObjectFromProxy(person);
1✔
234
                if (person instanceof Patient) {
1✔
235
                        return (Patient)person;
×
236
                }
237
                else {
238
                        return new Patient(person);
1✔
239
                }
240
        }
241

242
        /**
243
         * @see org.openmrs.api.PatientService#getAllPatients()
244
         */
245
        @Override
246
        @Transactional(readOnly = true)
247
        public List<Patient> getAllPatients() throws APIException {
248
                return Context.getPatientService().getAllPatients(false);
1✔
249
        }
250
        
251
        /**
252
         * @see org.openmrs.api.PatientService#getAllPatients(boolean)
253
         */
254
        @Override
255
        @Transactional(readOnly = true)
256
        public List<Patient> getAllPatients(boolean includeVoided) throws APIException {
257
                return dao.getAllPatients(includeVoided);
1✔
258
        }
259
        
260
        /**
261
         * @see org.openmrs.api.PatientService#getPatients(java.lang.String, java.lang.String,
262
         *      java.util.List, boolean)
263
         */
264
        @Override
265
        // TODO - search for usage with non-empty list of patient identifier types
266
        @Transactional(readOnly = true)
267
        public List<Patient> getPatients(String name, String identifier, List<PatientIdentifierType> identifierTypes,
268
                boolean matchIdentifierExactly) throws APIException {
269
                
270
                return Context.getPatientService().getPatients(name, identifier, identifierTypes, matchIdentifierExactly, 0, null);
1✔
271
        }
272
        
273
        /**
274
         * @see org.openmrs.api.PatientService#checkPatientIdentifiers(org.openmrs.Patient)
275
         */
276
        @Override
277
        @Transactional(readOnly = true)
278
        public void checkPatientIdentifiers(Patient patient) throws PatientIdentifierException {
279
                // check patient has at least one identifier
280
                if (!patient.getVoided() && patient.getActiveIdentifiers().isEmpty()) {
1✔
281
                        throw new InsufficientIdentifiersException("At least one nonvoided Patient Identifier is required");
1✔
282
                }
283

284
                final List<PatientIdentifier> patientIdentifiers = new ArrayList<>(patient.getIdentifiers());
1✔
285

286
                patientIdentifiers.stream()
1✔
287
                        .filter(pi -> !pi.getVoided())
1✔
288
                        .forEach(pi -> {
1✔
289
                                try {
290
                                        PatientIdentifierValidator.validateIdentifier(pi);
1✔
291
                                }
292
                                catch (BlankIdentifierException bie) {
1✔
293
                                        patient.removeIdentifier(pi);
1✔
294
                                        throw bie;
1✔
295
                                }
1✔
296
                        });
1✔
297

298
                checkForMissingRequiredIdentifiers(patientIdentifiers);
1✔
299

300
        }
1✔
301

302
        private void checkForMissingRequiredIdentifiers(List<PatientIdentifier> patientIdentifiers) {
303
                final Set<PatientIdentifierType> patientIdentifierTypes =
1✔
304
                                patientIdentifiers.stream()
1✔
305
                                                .map(PatientIdentifier::getIdentifierType)
1✔
306
                                                .collect(Collectors.toSet());
1✔
307

308
                final List<PatientIdentifierType> requiredTypes = this.getPatientIdentifierTypes(null, null, true, null);
1✔
309
                final Set<String> missingRequiredTypeNames =
1✔
310
                                requiredTypes.stream()
1✔
311
                                                .filter(requiredType -> !patientIdentifierTypes.contains(requiredType))
1✔
312
                                                .map(BaseOpenmrsMetadata::getName)
1✔
313
                                                .collect(Collectors.toSet());
1✔
314

315
                if(! missingRequiredTypeNames.isEmpty()) {
1✔
316
                        throw new MissingRequiredIdentifierException(
1✔
317
                                        "Patient is missing the following required identifier(s): " + String.join(", ", missingRequiredTypeNames));
1✔
318
                }
319
        }
1✔
320

321
        /**
322
         * @see org.openmrs.api.PatientService#voidPatient(org.openmrs.Patient, java.lang.String)
323
         */
324
        @Override
325
        public Patient voidPatient(Patient patient, String reason) throws APIException {
326
                if (patient == null) {
1✔
327
                        return null;
1✔
328
                }
329
                
330
                // patient and patientidentifier attributes taken care of by the BaseVoidHandler
331
                //call the DAO layer directly to avoid any further AOP around save*
332
                return dao.savePatient(patient);
1✔
333
        }
334
        
335
        /**
336
         * @see org.openmrs.api.PatientService#unvoidPatient(org.openmrs.Patient)
337
         */
338
        @Override
339
        public Patient unvoidPatient(Patient patient) throws APIException {
340
                if (patient == null) {
1✔
341
                        return null;
1✔
342
                }
343
                
344
                // patient and patientidentifier attributes taken care of by the BaseUnvoidHandler
345
                
346
                return Context.getPatientService().savePatient(patient);
1✔
347
        }
348
        
349
        /**
350
         * @see org.openmrs.api.PatientService#purgePatient(org.openmrs.Patient)
351
         */
352
        @Override
353
        public void purgePatient(Patient patient) throws APIException {
354
                dao.deletePatient(patient);
1✔
355
        }
1✔
356
        
357
        // patient identifier section
358
        
359
        /**
360
         * @see org.openmrs.api.PatientService#getPatientIdentifiers(java.lang.String, java.util.List,
361
         *      java.util.List, java.util.List, java.lang.Boolean)
362
         */
363
        @Override
364
        @Transactional(readOnly = true)
365
        public List<PatientIdentifier> getPatientIdentifiers(String identifier,
366
                List<PatientIdentifierType> patientIdentifierTypes, List<Location> locations, List<Patient> patients,
367
                Boolean isPreferred) throws APIException {
368
                
369
                if (patientIdentifierTypes == null) {
1✔
370
                        patientIdentifierTypes = new ArrayList<>();
1✔
371
                }
372
                
373
                if (locations == null) {
1✔
374
                        locations = new ArrayList<>();
1✔
375
                }
376
                
377
                if (patients == null) {
1✔
378
                        patients = new ArrayList<>();
1✔
379
                }
380
                
381
                return dao.getPatientIdentifiers(identifier, patientIdentifierTypes, locations, patients, isPreferred);
1✔
382
        }
383
        // end patient identifier section
384
        
385
        // patient identifier _type_ section
386
        
387
        /**
388
         * 
389
         * @see org.openmrs.api.PatientService#savePatientIdentifierType(org.openmrs.PatientIdentifierType)
390
         */
391
        @Override
392
        public PatientIdentifierType savePatientIdentifierType(PatientIdentifierType patientIdentifierType) throws APIException {
393
                checkIfPatientIdentifierTypesAreLocked();
1✔
394
                return dao.savePatientIdentifierType(patientIdentifierType);
1✔
395
        }
396
        
397
        /**
398
         * @see org.openmrs.api.PatientService#getAllPatientIdentifierTypes()
399
         */
400
        @Override
401
        @Transactional(readOnly = true)
402
        public List<PatientIdentifierType> getAllPatientIdentifierTypes() throws APIException {
403
                return Context.getPatientService().getAllPatientIdentifierTypes(false);
1✔
404
        }
405
        
406
        /**
407
         * @see org.openmrs.api.PatientService#getAllPatientIdentifierTypes(boolean)
408
         */
409
        @Override
410
        @Transactional(readOnly = true)
411
        public List<PatientIdentifierType> getAllPatientIdentifierTypes(boolean includeRetired) throws APIException {
412
                return dao.getAllPatientIdentifierTypes(includeRetired);
1✔
413
        }
414
        
415
        /**
416
         * @see org.openmrs.api.PatientService#getPatientIdentifierTypes(java.lang.String,
417
         *      java.lang.String, java.lang.Boolean, java.lang.Boolean)
418
         */
419
        @Override
420
        @Transactional(readOnly = true)
421
        public List<PatientIdentifierType> getPatientIdentifierTypes(String name, String format, Boolean required,
422
                Boolean hasCheckDigit) throws APIException {
423
                List<PatientIdentifierType> patientIdentifierTypes = dao.getPatientIdentifierTypes(name, format, required, hasCheckDigit);
1✔
424
                if (patientIdentifierTypes == null) {
1✔
425
                        return new ArrayList<>();
1✔
426
                }
427
                return patientIdentifierTypes;
1✔
428
        }
429
        
430
        /**
431
         * @see org.openmrs.api.PatientService#getPatientIdentifierType(java.lang.Integer)
432
         */
433
        @Override
434
        @Transactional(readOnly = true)
435
        public PatientIdentifierType getPatientIdentifierType(Integer patientIdentifierTypeId) throws APIException {
436
                return dao.getPatientIdentifierType(patientIdentifierTypeId);
1✔
437
        }
438
        
439
        /**
440
         * @see org.openmrs.api.PatientService#getPatientIdentifierTypeByName(java.lang.String)
441
         */
442
        @Override
443
        @Transactional(readOnly = true)
444
        public PatientIdentifierType getPatientIdentifierTypeByName(String name) throws APIException {
445
                List<PatientIdentifierType> types = getPatientIdentifierTypes(name, null, null, null);
1✔
446
                
447
                if (!types.isEmpty()) {
1✔
448
                        return types.get(0);
1✔
449
                }
450
                
451
                return null;
1✔
452
        }
453
        
454
        /**
455
         * @see org.openmrs.api.PatientService#retirePatientIdentifierType(org.openmrs.PatientIdentifierType,
456
         *      String)
457
         */
458
        @Override
459
        public PatientIdentifierType retirePatientIdentifierType(PatientIdentifierType patientIdentifierType, String reason)
460
                throws APIException {
461
                checkIfPatientIdentifierTypesAreLocked();
1✔
462
                if (reason == null || reason.length() < 1) {
1✔
463
                        throw new APIException("Patient.identifier.retire.reason", (Object[]) null);
1✔
464
                }
465
                
466
                patientIdentifierType.setRetired(true);
1✔
467
                patientIdentifierType.setRetiredBy(Context.getAuthenticatedUser());
1✔
468
                patientIdentifierType.setDateRetired(new Date());
1✔
469
                patientIdentifierType.setRetireReason(reason);
1✔
470
                return Context.getPatientService().savePatientIdentifierType(patientIdentifierType);
1✔
471
        }
472
        
473
        /**
474
         * @see org.openmrs.api.PatientService#unretirePatientIdentifierType(org.openmrs.PatientIdentifierType)
475
         */
476
        @Override
477
        public PatientIdentifierType unretirePatientIdentifierType(PatientIdentifierType patientIdentifierType)
478
                throws APIException {
479
                checkIfPatientIdentifierTypesAreLocked();
1✔
480
                patientIdentifierType.setRetired(false);
1✔
481
                patientIdentifierType.setRetiredBy(null);
1✔
482
                patientIdentifierType.setDateRetired(null);
1✔
483
                patientIdentifierType.setRetireReason(null);
1✔
484
                return Context.getPatientService().savePatientIdentifierType(patientIdentifierType);
1✔
485
        }
486
        
487
        /**
488
         * @see org.openmrs.api.PatientService#purgePatientIdentifierType(org.openmrs.PatientIdentifierType)
489
         */
490
        @Override
491
        public void purgePatientIdentifierType(PatientIdentifierType patientIdentifierType) throws APIException {
492
                checkIfPatientIdentifierTypesAreLocked();
1✔
493
                dao.deletePatientIdentifierType(patientIdentifierType);
1✔
494
        }
1✔
495
        
496
        // end patient identifier _type_ section
497
        
498
        /**
499
         * @see org.openmrs.api.PatientService#getPatients(java.lang.String)
500
         */
501
        @Override
502
        @Transactional(readOnly = true)
503
        public List<Patient> getPatients(String query) throws APIException {
504
                return Context.getPatientService().getPatients(query, 0, null);
1✔
505
        }
506
        
507
        /**
508
         * This default implementation simply looks at the OpenMRS internal id (patient_id). If the id
509
         * is null, assume this patient isn't found. If the patient_id is not null, try and find that id
510
         * in the database
511
         * 
512
         * @see org.openmrs.api.PatientService#getPatientByExample(org.openmrs.Patient)
513
         */
514
        @Override
515
        @Transactional(readOnly = true)
516
        public Patient getPatientByExample(Patient patientToMatch) throws APIException {
517
                if (patientToMatch == null || patientToMatch.getPatientId() == null) {
1✔
518
                        return null;
1✔
519
                }
520
                
521
                return Context.getPatientService().getPatient(patientToMatch.getPatientId());
1✔
522
        }
523
        
524
        /**
525
         * @see org.openmrs.api.PatientService#getDuplicatePatientsByAttributes(java.util.List)
526
         */
527
        @Override
528
        @Transactional(readOnly = true)
529
        public List<Patient> getDuplicatePatientsByAttributes(List<String> attributes) throws APIException {
530
                
531
                if (attributes == null || attributes.isEmpty()) {
1✔
532
                        throw new APIException("Patient.no.attribute", (Object[]) null);
1✔
533
                }
534
                
535
                return dao.getDuplicatePatientsByAttributes(attributes);
1✔
536
        }
537
        
538
        /**
539
         * generate a relationship hash for use in mergePatients; follows the convention:
540
         * [relationshipType][A|B][relativeId]
541
         * 
542
         * @param rel relationship under consideration
543
         * @param primary the focus of the hash
544
         * @return hash depicting relevant information to avoid duplicates
545
         */
546
        private String relationshipHash(Relationship rel, Person primary) {
547
                boolean isA = rel.getPersonA().equals(primary);
1✔
548
                return rel.getRelationshipType().getRelationshipTypeId().toString() + (isA ? "A" : "B")
1✔
549
                        + (isA ? rel.getPersonB().getPersonId().toString() : rel.getPersonA().getPersonId().toString());
1✔
550
        }
551

552
        /**
553
         * 1) Moves object (encounters/obs) pointing to <code>nonPreferred</code> to
554
         * <code>preferred</code> 2) Copies data (gender/birthdate/names/ids/etc) from
555
         * <code>nonPreferred</code> to <code>preferred</code> iff the data is missing or null in
556
         * <code>preferred</code> 3) <code>notPreferred</code> is marked as voided
557
         * 
558
         * @param preferred
559
         * @param notPreferred
560
         * @throws APIException
561
         * @see org.openmrs.api.PatientService#mergePatients(org.openmrs.Patient, org.openmrs.Patient)
562
         */
563
        @Override
564
        public void mergePatients(Patient preferred, Patient notPreferred) throws APIException, SerializationException {
565
                log.debug("Merging patients: (preferred)" + preferred.getPatientId() + ", (notPreferred) "
1✔
566
                        + notPreferred.getPatientId());
1✔
567
                if (preferred.getPatientId().equals(notPreferred.getPatientId())) {
1✔
568
                        log.debug("Merge operation cancelled: Cannot merge user" + preferred.getPatientId() + " to self");
1✔
569
                        throw new APIException("Patient.merge.cancelled", new Object[] { preferred.getPatientId() });
1✔
570
                }
571
                requireNoActiveOrderOfSameType(preferred,notPreferred);
1✔
572
                PersonMergeLogData mergedData = new PersonMergeLogData();
1✔
573
                mergeVisits(preferred, notPreferred, mergedData);
1✔
574
                mergeEncounters(preferred, notPreferred, mergedData);
1✔
575
                mergeProgramEnrolments(preferred, notPreferred, mergedData);
1✔
576
                mergeRelationships(preferred, notPreferred, mergedData);
1✔
577
                mergeObservationsNotContainedInEncounters(preferred, notPreferred, mergedData);
1✔
578
                mergeConditions(preferred, notPreferred, mergedData);
1✔
579
                mergeAllergies(preferred, notPreferred, mergedData);
1✔
580
                mergeMedicationDispenses(preferred, notPreferred, mergedData);
1✔
581
                mergeIdentifiers(preferred, notPreferred, mergedData);
1✔
582
                
583
                mergeNames(preferred, notPreferred, mergedData);
1✔
584
                mergeAddresses(preferred, notPreferred, mergedData);
1✔
585
                mergePersonAttributes(preferred, notPreferred, mergedData);
1✔
586
                mergeGenderInformation(preferred, notPreferred, mergedData);
1✔
587
                mergeDateOfBirth(preferred, notPreferred, mergedData);
1✔
588
                mergeDateOfDeath(preferred, notPreferred, mergedData);
1✔
589
                
590
                // void the non preferred patient
591
                Context.getPatientService().voidPatient(notPreferred, "Merged with patient #" + preferred.getPatientId());
1✔
592
                
593
                // void the person associated with not preferred patient
594
                Context.getPersonService().voidPerson(notPreferred,
1✔
595
                    "The patient corresponding to this person has been voided and Merged with patient #" + preferred.getPatientId());
1✔
596
                
597
                // associate the Users associated with the not preferred person, to the preferred person.
598
                changeUserAssociations(preferred, notPreferred, mergedData);
1✔
599
                
600
                // Save the newly update preferred patient
601
                // This must be called _after_ voiding the nonPreferred patient so that
602
                //  a "Duplicate Identifier" error doesn't pop up.
603
                savePatient(preferred);
1✔
604
                
605
                //save the person merge log
606
                PersonMergeLog personMergeLog = new PersonMergeLog();
1✔
607
                personMergeLog.setWinner(preferred);
1✔
608
                personMergeLog.setLoser(notPreferred);
1✔
609
                personMergeLog.setPersonMergeLogData(mergedData);
1✔
610
                Context.getPersonService().savePersonMergeLog(personMergeLog);
1✔
611
        }
1✔
612
        
613
        private void requireNoActiveOrderOfSameType(Patient patient1, Patient patient2) {
614
                String messageKey = "Patient.merge.cannotHaveSameTypeActiveOrders";
1✔
615
                List<Order> ordersByPatient1 = Context.getOrderService().getAllOrdersByPatient(patient1);
1✔
616
                List<Order> ordersByPatient2 = Context.getOrderService().getAllOrdersByPatient(patient2);
1✔
617
                ordersByPatient1.forEach((Order order1) -> ordersByPatient2.forEach((Order order2) -> {
1✔
618
                        if (order1.isActive() && order2.isActive() && order1.getOrderType().equals(order2.getOrderType())) {
1✔
619
                                Object[] parameters = { patient1.getPatientId(), patient2.getPatientId(), order1.getOrderType() };
1✔
620
                                String message = Context.getMessageSourceService().getMessage(messageKey, parameters,
1✔
621
                                                Context.getLocale());
1✔
622
                                log.debug(message);
1✔
623
                                throw new APIException(message);
1✔
624
                        }
625
                }));
1✔
626
        }
1✔
627

628
        private void mergeProgramEnrolments(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
629
                // copy all program enrollments
630
                ProgramWorkflowService programService = Context.getProgramWorkflowService();
1✔
631
                for (PatientProgram pp : programService.getPatientPrograms(notPreferred, null, null, null, null, null, false)) {
1✔
632
                        if (!pp.getVoided()) {
1✔
633
                                pp.setPatient(preferred);
1✔
634
                                log.debug("Moving patientProgram {} to {}", pp.getPatientProgramId(), preferred.getPatientId());
1✔
635
                                PatientProgram persisted = programService.savePatientProgram(pp);
1✔
636
                                mergedData.addMovedProgram(persisted.getUuid());
1✔
637
                        }
638
                }
1✔
639
        }
1✔
640
        
641
        private void mergeVisits(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
642
                // move all visits, including voided ones (encounters will be handled below)
643
                //TODO: this should be a copy, not a move
644
                
645
                VisitService visitService = Context.getVisitService();
1✔
646
                
647
                for (Visit visit : visitService.getVisitsByPatient(notPreferred, true, true)) {
1✔
648
                        log.debug("Merging visit {} to {}", visit.getVisitId(), preferred.getPatientId());
1✔
649
                        visit.setPatient(preferred);
1✔
650
                        Visit persisted = visitService.saveVisit(visit);
1✔
651
                        mergedData.addMovedVisit(persisted.getUuid());
1✔
652
                }
1✔
653
        }
1✔
654
        
655
        private void mergeEncounters(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
656
                // change all encounters. This will cascade to obs and orders contained in those encounters
657
                // TODO: this should be a copy, not a move
658
                EncounterService es = Context.getEncounterService();
1✔
659

660
                EncounterSearchCriteria notPreferredPatientEncounterSearchCriteria = new EncounterSearchCriteriaBuilder()
1✔
661
                                .setIncludeVoided(true)
1✔
662
                                .setPatient(notPreferred)
1✔
663
                                .createEncounterSearchCriteria();
1✔
664
                for (Encounter e : es.getEncounters(notPreferredPatientEncounterSearchCriteria)) {
1✔
665
                        e.setPatient(preferred);
1✔
666
                        log.debug("Merging encounter " + e.getEncounterId() + " to " + preferred.getPatientId());
1✔
667
                        Encounter persisted = es.saveEncounter(e);
1✔
668
                        mergedData.addMovedEncounter(persisted.getUuid());
1✔
669
                }
1✔
670
        }
1✔
671
        
672
        private void mergeRelationships(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
673
                // copy all relationships
674
                PersonService personService = Context.getPersonService();
1✔
675
                Set<String> existingRelationships = new HashSet<>();
1✔
676
                // fill in the existing relationships with hashes
677
                for (Relationship rel : personService.getRelationshipsByPerson(preferred)) {
1✔
678
                        existingRelationships.add(relationshipHash(rel, preferred));
1✔
679
                }
1✔
680
                // iterate over notPreferred's relationships and only copy them if they are needed
681
                for (Relationship rel : personService.getRelationshipsByPerson(notPreferred)) {
1✔
682
                        if (!rel.getVoided()) {
1✔
683
                                boolean personAisPreferred = rel.getPersonA().equals(preferred);
1✔
684
                                boolean personAisNotPreferred = rel.getPersonA().equals(notPreferred);
1✔
685
                                boolean personBisPreferred = rel.getPersonB().equals(preferred);
1✔
686
                                boolean personBisNotPreferred = rel.getPersonB().equals(notPreferred);
1✔
687
                                String relHash = relationshipHash(rel, notPreferred);
1✔
688
                                
689
                                if ((personAisPreferred && personBisNotPreferred) || (personBisPreferred && personAisNotPreferred)) {
1✔
690
                                        // void this relationship if it's between the preferred and notPreferred patients
691
                                        personService.voidRelationship(rel, "person " + (personAisNotPreferred ? "A" : "B")
1✔
692
                                                + " was merged to person " + (personAisPreferred ? "A" : "B"));
693
                                } else if (existingRelationships.contains(relHash)) {
1✔
694
                                        // void this relationship if it already exists between preferred and the other side
695
                                        personService.voidRelationship(rel, "person " + (personAisNotPreferred ? "A" : "B")
1✔
696
                                                + " was merged and a relationship already exists");
697
                                } else {
698
                                        // copy this relationship and replace notPreferred with preferred
699
                                        Relationship tmpRel = rel.copy();
1✔
700
                                        if (personAisNotPreferred) {
1✔
701
                                                tmpRel.setPersonA(preferred);
1✔
702
                                        }
703
                                        if (personBisNotPreferred) {
1✔
704
                                                tmpRel.setPersonB(preferred);
1✔
705
                                        }
706
                                        log.debug("Copying relationship " + rel.getRelationshipId() + " to " + preferred.getPatientId());
1✔
707
                                        Relationship persisted = personService.saveRelationship(tmpRel);
1✔
708
                                        mergedData.addCreatedRelationship(persisted.getUuid());
1✔
709
                                        // void the existing relationship to the notPreferred
710
                                        personService.voidRelationship(rel, "person " + (personAisNotPreferred ? "A" : "B")
1✔
711
                                                + " was merged, relationship copied to #" + tmpRel.getRelationshipId());
1✔
712
                                        // add the relationship hash to existing relationships
713
                                        existingRelationships.add(relHash);
1✔
714
                                }
715
                                mergedData.addVoidedRelationship(rel.getUuid());
1✔
716
                        }
717
                }
1✔
718
        }
1✔
719
        
720
        private void mergeObservationsNotContainedInEncounters(Patient preferred, Patient notPreferred,
721
                PersonMergeLogData mergedData) {
722
                // move all obs that weren't contained in encounters
723
                // TODO: this should be a copy, not a move
724
                ObsService obsService = Context.getObsService();
1✔
725
                for (Obs obs : obsService.getObservationsByPerson(notPreferred)) {
1✔
726
                        if (obs.getEncounter() == null && !obs.getVoided()) {
1✔
727
                                obs.setPerson(preferred);
1✔
728
                                Obs persisted = obsService.saveObs(obs, "Merged from patient #" + notPreferred.getPatientId());
1✔
729
                                mergedData.addMovedIndependentObservation(persisted.getUuid());
1✔
730
                        }
731
                }
1✔
732
        }
1✔
733
        
734
        private void mergeConditions(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
735
                // move all non-voided conditions (both patient-level and encounter-linked, since moving an
736
                // encounter does not reassign the patient on its conditions)
737
                // TODO: this should be a copy, not a move
738
                ConditionService conditionService = Context.getConditionService();
1✔
739
                for (Condition condition : conditionService.getAllConditions(notPreferred)) {
1✔
740
                        condition.setPatient(preferred);
1✔
741
                        log.debug("Merging condition {} to {}", condition.getConditionId(), preferred.getPatientId());
1✔
742
                        Condition persisted = conditionService.saveCondition(condition);
1✔
743
                        mergedData.addMovedCondition(persisted.getUuid());
1✔
744
                }
1✔
745
        }
1✔
746

747
        private void mergeAllergies(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
748
                // move all non-voided allergies, skipping any whose allergen the preferred patient already
749
                // records: a patient cannot hold two allergies for the same allergen (Allergies enforces this),
750
                // so moving a duplicate would make the survivor's allergy list fail to load afterwards
751
                // TODO: this should be a copy, not a move
752
                PatientService patientService = Context.getPatientService();
1✔
753
                // these are internal reads made on behalf of the merge; mergePatients is authorized with
754
                // Edit Patients, so acquire Get Allergies here rather than forcing the merging user to hold it
755
                Allergies preferredAllergies;
756
                Allergies notPreferredAllergies;
757
                try {
758
                        Context.addProxyPrivilege(PrivilegeConstants.GET_ALLERGIES);
1✔
759
                        preferredAllergies = patientService.getAllergies(preferred);
1✔
760
                        notPreferredAllergies = patientService.getAllergies(notPreferred);
1✔
761
                } finally {
762
                        Context.removeProxyPrivilege(PrivilegeConstants.GET_ALLERGIES);
1✔
763
                }
764
                for (Allergy allergy : notPreferredAllergies) {
1✔
765
                        if (preferredAllergies.containsAllergen(allergy)) {
1✔
766
                                log.debug("Skipping allergy {}; preferred patient {} already has the same allergen",
1✔
767
                                    allergy.getAllergyId(), preferred.getPatientId());
1✔
768
                                continue;
1✔
769
                        }
770
                        allergy.setPatient(preferred);
1✔
771
                        log.debug("Merging allergy {} to {}", allergy.getAllergyId(), preferred.getPatientId());
1✔
772
                        patientService.saveAllergy(allergy);
1✔
773
                        mergedData.addMovedAllergy(allergy.getUuid());
1✔
774
                }
1✔
775
        }
1✔
776

777
        private void mergeMedicationDispenses(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
778
                // move all non-voided medication dispenses (encounter-linked ones are not reassigned when their
779
                // encounter is moved, so they must be handled here)
780
                // TODO: this should be a copy, not a move
781
                MedicationDispenseService medicationDispenseService = Context.getMedicationDispenseService();
1✔
782
                MedicationDispenseCriteria criteria = new MedicationDispenseCriteriaBuilder().setPatient(notPreferred).build();
1✔
783
                for (MedicationDispense medicationDispense : medicationDispenseService.getMedicationDispenseByCriteria(criteria)) {
1✔
784
                        medicationDispense.setPatient(preferred);
1✔
785
                        log.debug("Merging medication dispense {} to {}", medicationDispense.getMedicationDispenseId(),
1✔
786
                            preferred.getPatientId());
1✔
787
                        MedicationDispense persisted = medicationDispenseService.saveMedicationDispense(medicationDispense);
1✔
788
                        mergedData.addMovedMedicationDispense(persisted.getUuid());
1✔
789
                }
1✔
790
        }
1✔
791

792
        private void mergeIdentifiers(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
793
                // move all identifiers
794
                // (must be done after all calls to services above so hbm doesn't try to save things prematurely (hacky)
795
                for (PatientIdentifier pi : notPreferred.getActiveIdentifiers()) {
1✔
796
                        PatientIdentifier tmpIdentifier = new PatientIdentifier();
1✔
797
                        tmpIdentifier.setIdentifier(pi.getIdentifier());
1✔
798
                        tmpIdentifier.setIdentifierType(pi.getIdentifierType());
1✔
799
                        tmpIdentifier.setLocation(pi.getLocation());
1✔
800
                        tmpIdentifier.setPatient(preferred);
1✔
801
                        boolean found = false;
1✔
802
                        for (PatientIdentifier preferredIdentifier : preferred.getIdentifiers()) {
1✔
803
                                if (preferredIdentifier.getIdentifier() != null
1✔
804
                                        && preferredIdentifier.getIdentifier().equals(tmpIdentifier.getIdentifier())
1✔
805
                                        && preferredIdentifier.getIdentifierType() != null
1✔
806
                                        && preferredIdentifier.getIdentifierType().equals(tmpIdentifier.getIdentifierType())) {
1✔
807
                                        found = true;
1✔
808
                                }
809
                        }
1✔
810
                        if (!found) {
1✔
811
                                tmpIdentifier.setIdentifierType(pi.getIdentifierType());
1✔
812
                                tmpIdentifier.setCreator(Context.getAuthenticatedUser());
1✔
813
                                tmpIdentifier.setDateCreated(new Date());
1✔
814
                                tmpIdentifier.setVoided(false);
1✔
815
                                tmpIdentifier.setVoidedBy(null);
1✔
816
                                tmpIdentifier.setVoidReason(null);
1✔
817
                                tmpIdentifier.setUuid(UUID.randomUUID().toString());
1✔
818
                                // we don't want to change the preferred identifier of the preferred patient
819
                                tmpIdentifier.setPreferred(false);
1✔
820
                                preferred.addIdentifier(tmpIdentifier);
1✔
821
                                mergedData.addCreatedIdentifier(tmpIdentifier.getUuid());
1✔
822
                                log.debug("Merging identifier " + tmpIdentifier.getIdentifier() + " to " + preferred.getPatientId());
1✔
823
                        }
824
                }
1✔
825
        }
1✔
826
        
827
        private void mergeDateOfDeath(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
828
                mergedData.setPriorDateOfDeath(preferred.getDeathDate());
1✔
829
                if (preferred.getDeathDate() == null) {
1✔
830
                        preferred.setDeathDate(notPreferred.getDeathDate());
1✔
831
                }
832
                
833
                if (preferred.getCauseOfDeath() != null) {
1✔
834
                        mergedData.setPriorCauseOfDeath(preferred.getCauseOfDeath().getUuid());
1✔
835
                }
836
                if (preferred.getCauseOfDeath() == null) {
1✔
837
                        preferred.setCauseOfDeath(notPreferred.getCauseOfDeath());
1✔
838
                }
839
        }
1✔
840
        
841
        private void mergeDateOfBirth(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
842
                mergedData.setPriorDateOfBirth(preferred.getBirthdate());
1✔
843
                mergedData.setPriorDateOfBirthEstimated(preferred.getBirthdateEstimated());
1✔
844
                if (preferred.getBirthdate() == null || (preferred.getBirthdateEstimated() && !notPreferred.getBirthdateEstimated())) {
1✔
845
                        preferred.setBirthdate(notPreferred.getBirthdate());
1✔
846
                        preferred.setBirthdateEstimated(notPreferred.getBirthdateEstimated());
1✔
847
                }
848
        }
1✔
849
        
850
        private void mergePersonAttributes(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
851
                // copy person attributes
852
                for (PersonAttribute attr : notPreferred.getAttributes()) {
1✔
853
                        if (!attr.getVoided()) {
1✔
854
                                PersonAttribute tmpAttr = attr.copy();
×
855
                                tmpAttr.setPerson(null);
×
856
                                tmpAttr.setUuid(UUID.randomUUID().toString());
×
857
                                preferred.addAttribute(tmpAttr);
×
858
                                mergedData.addCreatedAttribute(tmpAttr.getUuid());
×
859
                        }
860
                }
1✔
861
        }
1✔
862
        
863
        private void mergeGenderInformation(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
864
                // move all other patient info
865
                mergedData.setPriorGender(preferred.getGender());
1✔
866
                if (!"M".equals(preferred.getGender()) && !"F".equals(preferred.getGender())) {
1✔
867
                        preferred.setGender(notPreferred.getGender());
×
868
                }
869
        }
1✔
870
        
871
        private void mergeNames(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData) {
872
                // move all names
873
                // (must be done after all calls to services above so hbm doesn't try to save things prematurely (hacky)
874
                for (PersonName newName : notPreferred.getNames()) {
1✔
875
                        boolean containsName = false;
1✔
876
                        for (PersonName currentName : preferred.getNames()) {
1✔
877
                                containsName = currentName.equalsContent(newName);
1✔
878
                                if (containsName) {
1✔
879
                                        break;
1✔
880
                                }
881
                        }
1✔
882
                        if (!containsName) {
1✔
883
                                PersonName tmpName = constructTemporaryName(newName);
1✔
884
                                preferred.addName(tmpName);
1✔
885
                                mergedData.addCreatedName(tmpName.getUuid());
1✔
886
                                log.debug("Merging name " + newName.getGivenName() + " to " + preferred.getPatientId());
1✔
887
                        }
888
                }
1✔
889
        }
1✔
890
        
891
        private PersonName constructTemporaryName(PersonName newName) {
892
                PersonName tmpName = PersonName.newInstance(newName);
1✔
893
                tmpName.setPersonNameId(null);
1✔
894
                tmpName.setVoided(false);
1✔
895
                tmpName.setVoidedBy(null);
1✔
896
                tmpName.setVoidReason(null);
1✔
897
                // we don't want to change the preferred name of the preferred patient
898
                tmpName.setPreferred(false);
1✔
899
                tmpName.setUuid(UUID.randomUUID().toString());
1✔
900
                return tmpName;
1✔
901
        }
902
        
903
        private void mergeAddresses(Patient preferred, Patient notPreferred, PersonMergeLogData mergedData)
904
                throws SerializationException {
905
                // move all addresses
906
                // (must be done after all calls to services above so hbm doesn't try to save things prematurely (hacky)
907
                for (PersonAddress newAddress : notPreferred.getAddresses()) {
1✔
908
                        boolean containsAddress = false;
1✔
909
                        for (PersonAddress currentAddress : preferred.getAddresses()) {
1✔
910
                                containsAddress = currentAddress.equalsContent(newAddress);
1✔
911
                                if (containsAddress) {
1✔
912
                                        break;
1✔
913
                                }
914
                        }
1✔
915
                        if (!containsAddress) {
1✔
916
                                PersonAddress tmpAddress = (PersonAddress) newAddress.clone();
1✔
917
                                tmpAddress.setPersonAddressId(null);
1✔
918
                                tmpAddress.setVoided(false);
1✔
919
                                tmpAddress.setVoidedBy(null);
1✔
920
                                tmpAddress.setVoidReason(null);
1✔
921
                                tmpAddress.setPreferred(false); // addresses from non-preferred patient shouldn't be marked as preferred
1✔
922
                                tmpAddress.setUuid(UUID.randomUUID().toString());
1✔
923
                                preferred.addAddress(tmpAddress);
1✔
924
                                mergedData.addCreatedAddress(tmpAddress.getUuid());
1✔
925
                                log.debug("Merging address " + newAddress.getPersonAddressId() + " to " + preferred.getPatientId());
1✔
926
                        }
927
                }
1✔
928
                
929
                // copy person attributes
930
                for (PersonAttribute attr : notPreferred.getAttributes()) {
1✔
931
                        if (!attr.getVoided()) {
1✔
932
                                PersonAttribute tmpAttr = attr.copy();
1✔
933
                                tmpAttr.setPerson(null);
1✔
934
                                tmpAttr.setUuid(UUID.randomUUID().toString());
1✔
935
                                preferred.addAttribute(tmpAttr);
1✔
936
                                mergedData.addCreatedAttribute(tmpAttr.getUuid());
1✔
937
                        }
938
                }
1✔
939
                
940
                // move all other patient info
941
                mergedData.setPriorGender(preferred.getGender());
1✔
942
                if (!"M".equals(preferred.getGender()) && !"F".equals(preferred.getGender())) {
1✔
943
                        preferred.setGender(notPreferred.getGender());
×
944
                }
945

946
                mergedData.setPriorDateOfBirth(preferred.getBirthdate());
1✔
947
                mergedData.setPriorDateOfBirthEstimated(preferred.getBirthdateEstimated());
1✔
948
                if (preferred.getBirthdate() == null || (preferred.getBirthdateEstimated() && !notPreferred.getBirthdateEstimated())) {
1✔
949
                        preferred.setBirthdate(notPreferred.getBirthdate());
1✔
950
                        preferred.setBirthdateEstimated(notPreferred.getBirthdateEstimated());
1✔
951
                }
952
                mergedData.setPriorDateOfDeathEstimated(preferred.getDeathdateEstimated());
1✔
953
                if (preferred.getDeathdateEstimated() == null) {
1✔
954
                        preferred.setDeathdateEstimated(notPreferred.getDeathdateEstimated());
1✔
955
                }
956
                
957
                mergedData.setPriorDateOfDeath(preferred.getDeathDate());
1✔
958
                if (preferred.getDeathDate() == null) {
1✔
959
                        preferred.setDeathDate(notPreferred.getDeathDate());
1✔
960
                }
961
                
962
                if (preferred.getCauseOfDeath() != null) {
1✔
963
                        mergedData.setPriorCauseOfDeath(preferred.getCauseOfDeath().getUuid());
1✔
964
                }
965
                if (preferred.getCauseOfDeath() == null) {
1✔
966
                        preferred.setCauseOfDeath(notPreferred.getCauseOfDeath());
1✔
967
                }
968
                
969
                // void the non preferred patient
970
                Context.getPatientService().voidPatient(notPreferred, "Merged with patient #" + preferred.getPatientId());
1✔
971
                
972
                // void the person associated with not preferred patient
973
                Context.getPersonService().voidPerson(notPreferred,
1✔
974
                    "The patient corresponding to this person has been voided and Merged with patient #" + preferred.getPatientId());
1✔
975
                
976
                // associate the Users associated with the not preferred person, to the preferred person.
977
                changeUserAssociations(preferred, notPreferred, mergedData);
1✔
978
                
979
                // Save the newly update preferred patient
980
                // This must be called _after_ voiding the nonPreferred patient so that
981
                //  a "Duplicate Identifier" error doesn't pop up.
982
                savePatient(preferred);
1✔
983
                
984
                //save the person merge log
985
                PersonMergeLog personMergeLog = new PersonMergeLog();
1✔
986
                personMergeLog.setWinner(preferred);
1✔
987
                personMergeLog.setLoser(notPreferred);
1✔
988
                personMergeLog.setPersonMergeLogData(mergedData);
1✔
989
                Context.getPersonService().savePersonMergeLog(personMergeLog);
1✔
990
        }
1✔
991
        
992
        /**
993
         * Change user associations for notPreferred to preferred person.
994
         * 
995
         * @param preferred
996
         * @param notPreferred
997
         * @param mergedData a patient merge audit data object to update
998
         * @see PatientServiceImpl#mergePatients(Patient, Patient)
999
         */
1000
        private void changeUserAssociations(Patient preferred, Person notPreferred, PersonMergeLogData mergedData) {
1001
                UserService userService = Context.getUserService();
1✔
1002
                List<User> users = userService.getUsersByPerson(notPreferred, true);
1✔
1003
                for (User user : users) {
1✔
1004
                        user.setPerson(preferred);
1✔
1005
                        User persisted = userService.saveUser(user);
1✔
1006
                        if (mergedData != null) {
1✔
1007
                                mergedData.addMovedUser(persisted.getUuid());
1✔
1008
                        }
1009
                }
1✔
1010
        }
1✔
1011
        
1012
        /**
1013
         * This is the way to establish that a patient has left the care center. This API call is
1014
         * responsible for:
1015
         * <ol>
1016
         * <li>Closing workflow statuses</li>
1017
         * <li>Terminating programs</li>
1018
         * <li>Discontinuing orders</li>
1019
         * <li>Flagging patient table</li>
1020
         * <li>Creating any relevant observations about the patient (if applicable)</li>
1021
         * </ol>
1022
         * 
1023
         * @param patient - the patient who has exited care
1024
         * @param dateExited - the declared date/time of the patient's exit
1025
         * @param reasonForExit - the concept that corresponds with why the patient has been declared as
1026
         *            exited
1027
         * @throws APIException
1028
         */
1029
        public void exitFromCare(Patient patient, Date dateExited, Concept reasonForExit) throws APIException {
1030
                
1031
                if (patient == null) {
1✔
1032
                        throw new APIException("Patient.invalid.care", (Object[]) null);
×
1033
                }
1034
                if (dateExited == null) {
1✔
1035
                        throw new APIException("Patient.no.valid.dateExited", (Object[]) null);
×
1036
                }
1037
                if (reasonForExit == null) {
1✔
1038
                        throw new APIException("Patient.no.valid.reasonForExit", (Object[]) null);
×
1039
                }
1040
                
1041
                // need to create an observation to represent this (otherwise how
1042
                // will we know?)
1043
                saveReasonForExitObs(patient, dateExited, reasonForExit);
1✔
1044
        }
1✔
1045
        
1046
        /**
1047
         * TODO: Patients should actually be allowed to exit multiple times
1048
         * 
1049
         * @param patient
1050
         * @param exitDate
1051
         * @param cause
1052
         */
1053
        private void saveReasonForExitObs(Patient patient, Date exitDate, Concept cause) throws APIException {
1054
                
1055
                if (patient == null) {
1✔
1056
                        throw new APIException("Patient.null", (Object[]) null);
×
1057
                }
1058
                if (exitDate == null) {
1✔
1059
                        throw new APIException("Patient.exit.date.null", (Object[]) null);
×
1060
                }
1061
                if (cause == null) {
1✔
1062
                        throw new APIException("Patient.cause.null", (Object[]) null);
×
1063
                }
1064
                
1065
                // need to make sure there is an Obs that represents the patient's
1066
                // exit
1067
                log.debug("Patient is exiting, so let's make sure there's an Obs for it");
1✔
1068
                
1069
                String codProp = Context.getAdministrationService().getGlobalProperty("concept.reasonExitedCare");
1✔
1070
                Concept reasonForExit = Context.getConceptService().getConcept(codProp);
1✔
1071
                
1072
                if (reasonForExit != null) {
1✔
1073
                        List<Obs> obssExit = Context.getObsService().getObservationsByPersonAndConcept(patient, reasonForExit);
1✔
1074
                        if (obssExit != null) {
1✔
1075
                                if (obssExit.size() > 1) {
1✔
1076
                                        log.error("Multiple reasons for exit (" + obssExit.size() + ")?  Shouldn't be...");
×
1077
                                } else {
1078
                                        Obs obsExit;
1079
                                        if (obssExit.size() == 1) {
1✔
1080
                                                // already has a reason for exit - let's edit it.
1081
                                                log.debug("Already has a reason for exit, so changing it");
×
1082
                                                
1083
                                                obsExit = obssExit.iterator().next();
×
1084
                                                
1085
                                        } else {
1086
                                                // no reason for exit obs yet, so let's make one
1087
                                                log.debug("No reason for exit yet, let's create one.");
1✔
1088
                                                
1089
                                                obsExit = new Obs();
1✔
1090
                                                obsExit.setPerson(patient);
1✔
1091
                                                obsExit.setConcept(reasonForExit);
1✔
1092
                                                
1093
                                                Location loc = Context.getLocationService().getDefaultLocation();
1✔
1094
                                                
1095
                                                if (loc != null) {
1✔
1096
                                                        obsExit.setLocation(loc);
1✔
1097
                                                } else {
1098
                                                        log.error("Could not find a suitable location for which to create this new Obs");
×
1099
                                                }
1100
                                        }
1101
                                        
1102
                                        if (obsExit != null) {
1✔
1103
                                                // put the right concept and (maybe) text in this
1104
                                                // obs
1105
                                                obsExit.setValueCoded(cause);
1✔
1106
                                                obsExit.setValueCodedName(cause.getName()); // ABKTODO: presume current locale?
1✔
1107
                                                obsExit.setObsDatetime(exitDate);
1✔
1108
                                                Context.getObsService().saveObs(obsExit, "updated by PatientService.saveReasonForExit");
1✔
1109
                                        }
1110
                                }
1111
                        }
1112
                } else {
1✔
1113
                        log.debug("Reason for exit is null - should not have gotten here without throwing an error on the form.");
×
1114
                }
1115
                
1116
        }
1✔
1117
        
1118
        /**
1119
         * This is the way to establish that a patient has died. In addition to exiting the patient from
1120
         * care (see above), this method will also set the appropriate patient characteristics to
1121
         * indicate that they have died, when they died, etc.
1122
         * 
1123
         * @param patient - the patient who has died
1124
         * @param dateDied - the declared date/time of the patient's death
1125
         * @param causeOfDeath - the concept that corresponds with the reason the patient died
1126
         * @param otherReason - in case the causeOfDeath is 'other', a place to store more info
1127
         * @throws APIException
1128
         */
1129
        @Override
1130
        public void processDeath(Patient patient, Date dateDied, Concept causeOfDeath, String otherReason) throws APIException {
1131
                
1132
                if (patient != null && dateDied != null && causeOfDeath != null) {
1✔
1133
                        // set appropriate patient characteristics
1134
                        patient.setDead(true);
1✔
1135
                        patient.setDeathDate(dateDied);
1✔
1136
                        patient.setCauseOfDeath(causeOfDeath);
1✔
1137
                        this.savePatient(patient);
1✔
1138
                        saveCauseOfDeathObs(patient, dateDied, causeOfDeath, otherReason);
1✔
1139
                        
1140
                        // exit from program
1141
                        // first, need to get Concept for "Patient Died"
1142
                        String strPatientDied = Context.getAdministrationService().getGlobalProperty("concept.patientDied");
1✔
1143
                        Concept conceptPatientDied = Context.getConceptService().getConcept(strPatientDied);
1✔
1144
                        
1145
                        if (conceptPatientDied == null) {
1✔
1146
                                log.debug("ConceptPatientDied is null");
×
1147
                        }
1148
                        exitFromCare(patient, dateDied, conceptPatientDied);
1✔
1149
                        
1150
                } else {
1✔
1151
                        if (patient == null) {
1✔
1152
                                throw new APIException("Patient.invalid.dead", (Object[]) null);
1✔
1153
                        }
1154
                        if (dateDied == null) {
1✔
1155
                                throw new APIException("Patient.no.valid.dateDied", (Object[]) null);
1✔
1156
                        }
1157
                        if (causeOfDeath == null) {
1✔
1158
                                throw new APIException("Patient.no.valid.causeOfDeath", (Object[]) null);
1✔
1159
                        }
1160
                }
1161
        }
1✔
1162
        
1163
        /**
1164
         * @see org.openmrs.api.PatientService#saveCauseOfDeathObs(org.openmrs.Patient, java.util.Date,
1165
         *      org.openmrs.Concept, java.lang.String)
1166
         */
1167
        @Override
1168
        public void saveCauseOfDeathObs(Patient patient, Date deathDate, Concept cause, String otherReason) throws APIException {
1169
                
1170
                if (patient == null) {
1✔
1171
                        throw new APIException("Patient.null", (Object[]) null);
×
1172
                }
1173
                if (deathDate == null) {
1✔
1174
                        throw new APIException("Patient.death.date.null", (Object[]) null);
×
1175
                }
1176
                if (cause == null) {
1✔
1177
                        throw new APIException("Patient.cause.null", (Object[]) null);
×
1178
                }
1179
                
1180
                if (!patient.getDead()) {
1✔
1181
                        patient.setDead(true);
×
1182
                        patient.setDeathDate(deathDate);
×
1183
                        patient.setCauseOfDeath(cause);
×
1184
                }
1185
                
1186
                log.debug("Patient is dead, so let's make sure there's an Obs for it");
1✔
1187
                // need to make sure there is an Obs that represents the patient's
1188
                // cause of death, if applicable
1189
                
1190
                String codProp = Context.getAdministrationService().getGlobalProperty("concept.causeOfDeath");
1✔
1191
                
1192
                Concept causeOfDeath = Context.getConceptService().getConcept(codProp);
1✔
1193
                
1194
                if (causeOfDeath != null) {
1✔
1195
                        List<Obs> obssDeath = Context.getObsService().getObservationsByPersonAndConcept(patient, causeOfDeath);
1✔
1196
                        if (obssDeath != null) {
1✔
1197
                                if (obssDeath.size() > 1) {
1✔
1198
                                        log.error("Multiple causes of death (" + obssDeath.size() + ")?  Shouldn't be...");
×
1199
                                } else {
1200
                                        Obs obsDeath;
1201
                                        if (obssDeath.size() == 1) {
1✔
1202
                                                // already has a cause of death - let's edit it.
1203
                                                log.debug("Already has a cause of death, so changing it");
×
1204
                                                
1205
                                                obsDeath = obssDeath.iterator().next();
×
1206
                                                
1207
                                        } else {
1208
                                                // no cause of death obs yet, so let's make one
1209
                                                log.debug("No cause of death yet, let's create one.");
1✔
1210
                                                
1211
                                                obsDeath = new Obs();
1✔
1212
                                                obsDeath.setPerson(patient);
1✔
1213
                                                obsDeath.setConcept(causeOfDeath);
1✔
1214
                                                Location location = Context.getLocationService().getDefaultLocation();
1✔
1215
                                                if (location != null) {
1✔
1216
                                                        obsDeath.setLocation(location);
1✔
1217
                                                } else {
1218
                                                        log.error("Could not find a suitable location for which to create this new Obs");
×
1219
                                                }
1220
                                        }
1221
                                        
1222
                                        // put the right concept and (maybe) text in this obs
1223
                                        Concept currCause = patient.getCauseOfDeath();
1✔
1224
                                        if (currCause == null) {
1✔
1225
                                                // set to NONE
1226
                                                log.debug("Current cause is null, attempting to set to NONE");
×
1227
                                                String noneConcept = Context.getAdministrationService().getGlobalProperty("concept.none");
×
1228
                                                currCause = Context.getConceptService().getConcept(noneConcept);
×
1229
                                        }
1230
                                        
1231
                                        if (currCause != null) {
1✔
1232
                                                log.debug("Current cause is not null, setting to value_coded");
1✔
1233
                                                obsDeath.setValueCoded(currCause);
1✔
1234
                                                obsDeath.setValueCodedName(currCause.getName()); // ABKTODO: presume current locale?
1✔
1235
                                                
1236
                                                Date dateDeath = patient.getDeathDate();
1✔
1237
                                                if (dateDeath == null) {
1✔
1238
                                                        dateDeath = new Date();
×
1239
                                                }
1240
                                                obsDeath.setObsDatetime(dateDeath);
1✔
1241
                                                
1242
                                                // check if this is an "other" concept - if so, then
1243
                                                // we need to add value_text
1244
                                                String otherConcept = Context.getAdministrationService().getGlobalProperty("concept.otherNonCoded");
1✔
1245
                                                Concept conceptOther = Context.getConceptService().getConcept(otherConcept);
1✔
1246
                                                if (conceptOther != null) {
1✔
1247
                                                        if (conceptOther.equals(currCause)) {
1✔
1248
                                                                // seems like this is an other concept -
1249
                                                                // let's try to get the "other" field info
1250
                                                                log.debug("Setting value_text as " + otherReason);
×
1251
                                                                obsDeath.setValueText(otherReason);
×
1252
                                                        } else {
1253
                                                                log.debug("New concept is NOT the OTHER concept, so setting to blank");
1✔
1254
                                                                obsDeath.setValueText("");
1✔
1255
                                                        }
1256
                                                } else {
1257
                                                        log.debug("Don't seem to know about an OTHER concept, so deleting value_text");
×
1258
                                                        obsDeath.setValueText("");
×
1259
                                                }
1260
                                                
1261
                                                Context.getObsService().saveObs(obsDeath, "updated by PatientService.saveCauseOfDeathObs");
1✔
1262
                                        } else {
1✔
1263
                                                log.debug("Current cause is still null - aborting mission");
×
1264
                                        }
1265
                                }
1266
                        }
1267
                } else {
1✔
1268
                        log.debug("Cause of death is null - should not have gotten here without throwing an error on the form.");
×
1269
                }
1270
        }
1✔
1271
        
1272
        /**
1273
         * @see org.openmrs.api.PatientService#getPatientByUuid(java.lang.String)
1274
         */
1275
        @Override
1276
        @Transactional(readOnly = true)
1277
        public Patient getPatientByUuid(String uuid) throws APIException {
1278
                return dao.getPatientByUuid(uuid);
1✔
1279
        }
1280
        
1281
        @Override
1282
        @Transactional(readOnly = true)
1283
        public PatientIdentifier getPatientIdentifierByUuid(String uuid) throws APIException {
1284
                return dao.getPatientIdentifierByUuid(uuid);
1✔
1285
        }
1286
        
1287
        /**
1288
         * @see org.openmrs.api.PatientService#getPatientIdentifierTypeByUuid(java.lang.String)
1289
         */
1290
        @Override
1291
        @Transactional(readOnly = true)
1292
        public PatientIdentifierType getPatientIdentifierTypeByUuid(String uuid) throws APIException {
1293
                return dao.getPatientIdentifierTypeByUuid(uuid);
1✔
1294
        }
1295
        
1296
        /**
1297
         * @see org.openmrs.api.PatientService#getDefaultIdentifierValidator()
1298
         */
1299
        @Override
1300
        @Transactional(readOnly = true)
1301
        public IdentifierValidator getDefaultIdentifierValidator() {
1302
                String defaultPIV = Context.getAdministrationService().getGlobalProperty(
×
1303
                    OpenmrsConstants.GLOBAL_PROPERTY_DEFAULT_PATIENT_IDENTIFIER_VALIDATOR, "");
1304
                
1305
                try {
1306
                        return identifierValidators.get(Class.forName(defaultPIV));
×
1307
                }
1308
                catch (ClassNotFoundException e) {
×
1309
                        log.error("Global Property " + OpenmrsConstants.GLOBAL_PROPERTY_DEFAULT_PATIENT_IDENTIFIER_VALIDATOR
×
1310
                                + " not set to an actual class.", e);
1311
                        return identifierValidators.get(LuhnIdentifierValidator.class);
×
1312
                }
1313
        }
1314
        
1315
        /**
1316
         * @see org.openmrs.api.PatientService#getIdentifierValidator(java.lang.String)
1317
         */
1318
        @Override
1319
        public IdentifierValidator getIdentifierValidator(Class<IdentifierValidator> identifierValidator) {
1320
                return identifierValidators.get(identifierValidator);
1✔
1321
        }
1322
        
1323
        public Map<Class<? extends IdentifierValidator>, IdentifierValidator> getIdentifierValidators() {
1324
                if (identifierValidators == null) {
1✔
1325
                        identifierValidators = new LinkedHashMap<>();
1✔
1326
                }
1327
                return identifierValidators;
1✔
1328
        }
1329
        
1330
        /**
1331
         * ADDs identifierValidators, doesn't replace them
1332
         * 
1333
         * @param identifierValidators
1334
         */
1335
        public void setIdentifierValidators(Map<Class<? extends IdentifierValidator>, IdentifierValidator> identifierValidators) {
1336
                if (identifierValidators == null) {
1✔
1337
                        PatientServiceImpl.setStaticIdentifierValidators(null);
×
1338
                        return;
×
1339
                }
1340
                for (Map.Entry<Class<? extends IdentifierValidator>, IdentifierValidator> entry : identifierValidators.entrySet()) {
1✔
1341
                        getIdentifierValidators().put(entry.getKey(), entry.getValue());
1✔
1342
                }
1✔
1343
        }
1✔
1344
        
1345
        /**
1346
         * Sets identifierValidators using static method
1347
         *
1348
         * @param currentIdentifierValidators
1349
         */
1350
        private static void setStaticIdentifierValidators(
1351
                Map<Class<? extends IdentifierValidator>, IdentifierValidator> currentIdentifierValidators) {
1352
                PatientServiceImpl.identifierValidators = currentIdentifierValidators;
×
1353
        }
×
1354
        
1355
        /**
1356
         * @see org.openmrs.api.PatientService#getAllIdentifierValidators()
1357
         */
1358
        @Override
1359
        public Collection<IdentifierValidator> getAllIdentifierValidators() {
1360
                return identifierValidators.values();
1✔
1361
        }
1362
        
1363
        /**
1364
         * @see org.openmrs.api.PatientService#getIdentifierValidator(java.lang.String)
1365
         */
1366
        @Override
1367
        @SuppressWarnings("unchecked")
1368
        @Transactional(readOnly = true)
1369
        public IdentifierValidator getIdentifierValidator(String pivClassName) {
1370
                if (StringUtils.isBlank(pivClassName)) {
1✔
1371
                        return null;
1✔
1372
                }
1373
                
1374
                try {
1375
                        return getIdentifierValidator((Class<IdentifierValidator>) Context.loadClass(pivClassName));
1✔
1376
                }
1377
                catch (ClassNotFoundException e) {
1✔
1378
                        throw new PatientIdentifierException("Could not find patient identifier validator " + pivClassName, e);
1✔
1379
                }
1380
        }
1381
        
1382
        /**
1383
         * @see org.openmrs.api.PatientService#isIdentifierInUseByAnotherPatient(org.openmrs.PatientIdentifier)
1384
         */
1385
        @Override
1386
        @Transactional(readOnly = true)
1387
        public boolean isIdentifierInUseByAnotherPatient(PatientIdentifier patientIdentifier) {
1388
                return dao.isIdentifierInUseByAnotherPatient(patientIdentifier);
1✔
1389
        }
1390
        
1391
        /**
1392
         * @see org.openmrs.api.PatientService#getPatientIdentifier(java.lang.Integer)
1393
         */
1394
        @Override
1395
        @Transactional(readOnly = true)
1396
        public PatientIdentifier getPatientIdentifier(Integer patientIdentifierId) throws APIException {
1397
                return dao.getPatientIdentifier(patientIdentifierId);
1✔
1398
        }
1399
        
1400
        /**
1401
         * @see org.openmrs.api.PatientService#voidPatientIdentifier(org.openmrs.PatientIdentifier,
1402
         *      java.lang.String)
1403
         */
1404
        @Override
1405
        public PatientIdentifier voidPatientIdentifier(PatientIdentifier patientIdentifier, String reason) throws APIException {
1406
                
1407
                if (patientIdentifier == null || StringUtils.isBlank(reason)) {
1✔
1408
                        throw new APIException("Patient.identifier.cannot.be.null", (Object[]) null);
1✔
1409
                }
1410
                return Context.getPatientService().savePatientIdentifier(patientIdentifier);
1✔
1411
                
1412
        }
1413
        
1414
        /**
1415
         * @see org.openmrs.api.PatientService#mergePatients(org.openmrs.Patient, java.util.List)
1416
         */
1417
        @Override
1418
        public void mergePatients(Patient preferred, List<Patient> notPreferred) throws APIException, SerializationException {
1419
                
1420
                for (Patient nonPreferred : notPreferred) {
1✔
1421
                        mergePatients(preferred, nonPreferred);
1✔
1422
                }
1✔
1423
        }
1✔
1424
        
1425
        /**
1426
         * @see org.openmrs.api.PatientService#savePatientIdentifier(org.openmrs.PatientIdentifier)
1427
         */
1428
        @Override
1429
        public PatientIdentifier savePatientIdentifier(PatientIdentifier patientIdentifier) throws APIException {
1430
                //if the argument or the following required fields are not specified
1431
                PatientIdentifierType.LocationBehavior locationBehavior = null;
1✔
1432
                if (patientIdentifier != null) {
1✔
1433
                        locationBehavior = patientIdentifier.getIdentifierType().getLocationBehavior();
1✔
1434
                }
1435
                
1436
                if (patientIdentifier == null
1✔
1437
                        || patientIdentifier.getPatient() == null
1✔
1438
                        || patientIdentifier.getIdentifierType() == null
1✔
1439
                        || StringUtils.isBlank(patientIdentifier.getIdentifier())
1✔
1440
                        || (locationBehavior == PatientIdentifierType.LocationBehavior.REQUIRED && patientIdentifier.getLocation() == null)) {
×
1441
                        throw new APIException("Patient.identifier.null", (Object[]) null);
1✔
1442
                }
1443
                if (patientIdentifier.getPatientIdentifierId() == null) {
1✔
1444
                        Context.requirePrivilege(PrivilegeConstants.ADD_PATIENT_IDENTIFIERS);
1✔
1445
                } else {
1446
                        Context.requirePrivilege(PrivilegeConstants.EDIT_PATIENT_IDENTIFIERS);
1✔
1447
                }
1448
                
1449
                return dao.savePatientIdentifier(patientIdentifier);
1✔
1450
        }
1451
        
1452
        /**
1453
         * @see org.openmrs.api.PatientService#purgePatientIdentifier(org.openmrs.PatientIdentifier)
1454
         */
1455
        @Override
1456
        public void purgePatientIdentifier(PatientIdentifier patientIdentifier) throws APIException {
1457
                
1458
                dao.deletePatientIdentifier(patientIdentifier);
1✔
1459
                
1460
        }
1✔
1461
        
1462
        /**
1463
         * @see org.openmrs.api.PatientService#getAllergies(org.openmrs.Patient)
1464
         */
1465
        @Override
1466
        @Transactional(readOnly = true)
1467
        public Allergies getAllergies(Patient patient) {
1468
                if (patient == null) {
1✔
1469
                        throw new IllegalArgumentException("An existing (NOT NULL) patient is required to get allergies");
×
1470
                }
1471
                
1472
                Allergies allergies = new Allergies();
1✔
1473
                List<Allergy> allergyList = dao.getAllergies(patient);
1✔
1474
                if (!allergyList.isEmpty()) {
1✔
1475
                        allergies.addAll(allergyList);
1✔
1476
                } else {
1477
                        String status = dao.getAllergyStatus(patient);
1✔
1478
                        if (Allergies.NO_KNOWN_ALLERGIES.equals(status)) {
1✔
1479
                                allergies.confirmNoKnownAllergies();
1✔
1480
                        }
1481
                }
1482
                return allergies;
1✔
1483
        }
1484
        
1485
        /**
1486
         * @see org.openmrs.api.PatientService#setAllergies(org.openmrs.Patient,
1487
         *      org.openmrs.Allergies)
1488
         */
1489
        @Override
1490
        public Allergies setAllergies(Patient patient, Allergies allergies) {
1491
                //NOTE We neither delete nor edit allergies. We instead void them.
1492
                //Because we shield the API users from this business logic,
1493
                //we end up with the complicated code below. :)
1494
                
1495
                //get the current allergies as stored in the database
1496
                List<Allergy> dbAllergyList = getAllergies(patient);
1✔
1497
                for (Allergy originalAllergy : dbAllergyList) {
1✔
1498
                        //check if we still have each allergy, else it has just been deleted
1499
                        if (allergies.contains(originalAllergy)) {
1✔
1500
                                //we still have this allergy, check if it has been edited/changed
1501
                                Allergy potentiallyEditedAllergy = allergies.getAllergy(originalAllergy.getAllergyId());
1✔
1502
                                if (!potentiallyEditedAllergy.hasSameValues(originalAllergy)) {
1✔
1503
                                        //allergy has been edited, so void it and create a new one with the current values
1504
                                        Allergy newAllergy = new Allergy();
1✔
1505
                                        try {
1506
                                                //remove the edited allergy from our current list, and void id
1507
                                                allergies.remove(potentiallyEditedAllergy);
1✔
1508
                                                
1509
                                                //copy values from edited allergy, and add it to the current list
1510
                                                newAllergy.copy(potentiallyEditedAllergy);
1✔
1511
                                                allergies.add(newAllergy);
1✔
1512
                                                
1513
                                                //we void its original values, as came from the database, 
1514
                                                //instead the current ones which have just been copied 
1515
                                                //into the new allergy we have just created above
1516
                                                voidAllergy(originalAllergy);
1✔
1517
                                        }
1518
                                        catch (Exception ex) {
×
1519
                                                throw new APIException("Failed to copy edited values", ex);
×
1520
                                        }
1✔
1521
                                }
1✔
1522
                                continue;
1523
                        }
1524
                        
1525
                        //void the allergy that has been deleted
1526
                        voidAllergy(originalAllergy);
1✔
1527
                }
1✔
1528
                
1529
                for (Allergy allergy : allergies) {
1✔
1530
                        if (allergy.getAllergyId() == null && allergy.getAllergen().getCodedAllergen() == null
1✔
1531
                                && StringUtils.isNotBlank(allergy.getAllergen().getNonCodedAllergen())) {
1✔
1532
                                
1533
                                Concept otherNonCoded = Context.getConceptService().getConceptByUuid(Allergen.getOtherNonCodedConceptUuid());
1✔
1534
                                if (otherNonCoded == null) {
1✔
1535
                                        throw new APIException("Can't find concept with uuid:" + Allergen.getOtherNonCodedConceptUuid());
×
1536
                                }
1537
                                allergy.getAllergen().setCodedAllergen(otherNonCoded);
1✔
1538
                        }
1539
                }
1✔
1540
                
1541
                return dao.saveAllergies(patient, allergies);
1✔
1542
        }
1543
        
1544
        /**
1545
         * Voids a given allergy
1546
         * 
1547
         * @param allergy the allergy to void
1548
         */
1549
        private void voidAllergy(Allergy allergy) {
1550
                allergy.setVoided(true);
1✔
1551
                allergy.setVoidedBy(Context.getAuthenticatedUser());
1✔
1552
                allergy.setDateVoided(new Date());
1✔
1553
                allergy.setVoidReason("Voided by API");
1✔
1554
                dao.saveAllergy(allergy);
1✔
1555
        }
1✔
1556
        
1557
        /**
1558
         * @see org.openmrs.api.PatientService#getAllergy(java.lang.Integer)
1559
         */
1560
        @Override
1561
        @Transactional(readOnly = true)
1562
        public Allergy getAllergy(Integer allergyId) throws APIException {
1563
                return dao.getAllergy(allergyId);
×
1564
        }
1565
        
1566
        /**
1567
         * @see org.openmrs.api.PatientService#getAllergyByUuid(java.lang.String)
1568
         */
1569
        @Override
1570
        @Transactional(readOnly = true)
1571
        public Allergy getAllergyByUuid(String uuid) throws APIException {
1572
                return dao.getAllergyByUuid(uuid);
1✔
1573
        }
1574
        
1575
        /**
1576
         * @see org.openmrs.api.PatientService#saveAllergy(org.openmrs.Allergy)
1577
         */
1578
        @Override
1579
        public void saveAllergy(Allergy allergy) throws APIException {
1580

1581
                dao.saveAllergy(allergy);
1✔
1582
        }
1✔
1583
        
1584
        /**
1585
         * @see org.openmrs.api.PatientService#removeAllergy(org.openmrs.Allergy,
1586
         *      java.lang.String)
1587
         */
1588
        @Override
1589
        public void removeAllergy(Allergy allergy, String reason) throws APIException {
1590
                voidAllergy(allergy, reason);
×
1591
        }
×
1592
        
1593
        /**
1594
         * @see org.openmrs.api.PatientService#voidAllergy(org.openmrs.Allergy,
1595
         *      java.lang.String)
1596
         */
1597
        @Override
1598
        public void voidAllergy(Allergy allergy, String reason) throws APIException {
1599

1600
                allergy.setVoided(true);
1✔
1601
                allergy.setVoidedBy(Context.getAuthenticatedUser());
1✔
1602
                allergy.setDateVoided(new Date());
1✔
1603
                allergy.setVoidReason(reason);
1✔
1604
                dao.saveAllergy(allergy);
1✔
1605
        }
1✔
1606
        
1607
        /**
1608
         * @see PatientService#getCountOfPatients(String)
1609
         */
1610
        @Override
1611
        @Transactional(readOnly = true)
1612
        public Integer getCountOfPatients(String query) {
1613
                int count = 0;
1✔
1614
                if (StringUtils.isBlank(query)) {
1✔
1615
                        return count;
×
1616
                }
1617
                
1618
                return OpenmrsUtil.convertToInteger(dao.getCountOfPatients(query));
1✔
1619
        }
1620
        
1621
        /**
1622
         * @see PatientService#getCountOfPatients(String, boolean)
1623
         */
1624
        @Override
1625
        @Transactional(readOnly = true)
1626
        public Integer getCountOfPatients(String query, boolean includeVoided) {
1627
                int count = 0;
×
1628
                if (StringUtils.isBlank(query)) {
×
1629
                        return count;
×
1630
                }
1631
                
1632
                return OpenmrsUtil.convertToInteger(dao.getCountOfPatients(query, includeVoided));
×
1633
        }
1634
        
1635
        /**
1636
         * @see PatientService#getPatients(String, Integer, Integer)
1637
         */
1638
        @Override
1639
        @Transactional(readOnly = true)
1640
        public List<Patient> getPatients(String query, Integer start, Integer length) throws APIException {
1641
                List<Patient> patients = new ArrayList<>();
1✔
1642
                if (StringUtils.isBlank(query)) {
1✔
1643
                        return patients;
×
1644
                }
1645
                
1646
                return dao.getPatients(query, start, length);
1✔
1647
        }
1648
        
1649
        /**
1650
         * @see PatientService#getPatients(String, boolean, Integer, Integer)
1651
         */
1652
        @Override
1653
        @Transactional(readOnly = true)
1654
        public List<Patient> getPatients(String query, boolean includeVoided, Integer start, Integer length) throws APIException {
1655
                if (StringUtils.isBlank(query)) {
×
1656
                        return Collections.emptyList();
×
1657
                }
1658
                
1659
                return dao.getPatients(query, includeVoided, start, length);
×
1660
        }
1661
        
1662
        /**
1663
         * @see PatientService#getPatients(String, String, List, boolean, Integer, Integer)
1664
         */
1665
        // TODO - search for usage with non-empty list of patient identifier types - not used
1666
        @Override
1667
        @Transactional(readOnly = true)
1668
        public List<Patient> getPatients(String name, String identifier, List<PatientIdentifierType> identifierTypes,
1669
                boolean matchIdentifierExactly, Integer start, Integer length) throws APIException {
1670
                
1671
                if(identifierTypes == null) {
1✔
1672
                        return dao.getPatients(name != null ? name : identifier, start, length);
1✔
1673
                }
1674
                else {
1675
                        return dao.getPatients(name != null ? name : identifier, identifierTypes, matchIdentifierExactly, start, length);
1✔
1676
                }
1677
        }
1678
        
1679
        /**
1680
         * @see PatientService#checkIfPatientIdentifierTypesAreLocked()
1681
         */
1682
        @Override
1683
        public void checkIfPatientIdentifierTypesAreLocked() {
1684
                String locked = Context.getAdministrationService().getGlobalProperty(
1✔
1685
                    OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_IDENTIFIER_TYPES_LOCKED, "false");
1686
                if ("true".equalsIgnoreCase(locked)) {
1✔
1687
                        throw new PatientIdentifierTypeLockedException();
1✔
1688
                }
1689
        }
1✔
1690

1691
        /**
1692
         * @see PatientService#getPatientIdentifiersByPatientProgram(org.openmrs.PatientProgram)
1693
         */
1694
        public List<PatientIdentifier> getPatientIdentifiersByPatientProgram(PatientProgram patientProgram) {
1695
                return dao.getPatientIdentifierByProgram(patientProgram);
1✔
1696
        }
1697
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc