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

openmrs / openmrs-core / 27660627112

17 Jun 2026 01:57AM UTC coverage: 63.664% (+0.2%) from 63.512%
27660627112

push

github

web-flow
mergePatients does not reassign Conditions, Allergies, or MedicationDispenses to the surviving patient (#6198)

* mergePatients now reassigns Conditions, Allergies and MedicationDispenses to the surviving patient

PatientServiceImpl.mergePatients had no reassignment helpers for the
patient-scoped Conditions, Allergies and MedicationDispenses tables, which
were added to core after the merge logic was written. After a merge these
clinical records stayed attached to the now-voided non-preferred patient and
silently disappeared from the surviving patient's record - a clinical-data-loss
and patient-safety concern (e.g. a hidden allergy).

Add mergeConditions, mergeAllergies and mergeMedicationDispenses helpers that
move each non-voided record from the non-preferred to the preferred patient via
the corresponding services, mirroring the existing
mergeObservationsNotContainedInEncounters pattern. Saving through the services
keeps the changes audited and emits the normal save events. The moves are
recorded in new PersonMergeLogData collections so they appear in the
PersonMergeLog.

Allergies are deduplicated by allergen: an allergy whose allergen the preferred
patient already records is not moved, because a patient cannot hold two
allergies for the same allergen and a duplicate would make the survivor's
allergy list fail to load afterwards.

Fixes #6194

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Keep @since on the new PersonMergeLogData accessors only, not the private fields

The moved* fields are private (not API and not rendered by Javadoc), so the
field-level @since 2.9.0 tags were redundant. The public getters/adders keep
@since 2.9.0, matching the codebase convention (Patient, Condition, Allergy
attach @since to accessors, not backing fields).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

52 of 55 new or added lines in 2 files covered. (94.55%)

3 existing lines in 2 files now uncovered.

23909 of 37555 relevant lines covered (63.66%)

0.64 hits per line

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

85.29
/api/src/main/java/org/openmrs/util/HandlerUtil.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.util;
11

12
import java.util.ArrayList;
13
import java.util.Comparator;
14
import java.util.List;
15
import java.util.Map;
16
import java.util.WeakHashMap;
17

18
import org.openmrs.annotation.Handler;
19
import org.openmrs.api.APIException;
20
import org.openmrs.api.context.Context;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23
import org.springframework.context.ApplicationListener;
24
import org.springframework.context.event.ContextRefreshedEvent;
25
import org.springframework.stereotype.Component;
26

27
/**
28
 * Utility class that provides useful methods for working with classes that are annotated with the
29
 * {@link Handler} annotation
30
 * 
31
 * @since 1.5
32
 */
33
@Component
34
public class HandlerUtil implements ApplicationListener<ContextRefreshedEvent> {
1✔
35
        
36
        private static final Logger log = LoggerFactory.getLogger(HandlerUtil.class);
1✔
37
        
38
        private static volatile Map<Key, List<?>> cachedHandlers = new WeakHashMap<>();
1✔
39
        
40
        private static class Key {
41
                
42
                public final Class<?> handlerType;
43
                
44
                public final Class<?> type;
45
                
46
                public Key(Class<?> handlerType, Class<?> type) {
1✔
47
                        this.handlerType = handlerType;
1✔
48
                        this.type = type;
1✔
49
                }
1✔
50
                
51
                @Override
52
                public int hashCode() {
53
                        final int prime = 31;
1✔
54
                        int result = 1;
1✔
55
                        result = prime * result + ((handlerType == null) ? 0 : handlerType.hashCode());
1✔
56
                        result = prime * result + ((type == null) ? 0 : type.hashCode());
1✔
57
                        return result;
1✔
58
                }
59
                
60
                @Override
61
                public boolean equals(Object obj) {
62
                        if (this == obj) {
1✔
63
                                return true;
×
64
                        }
65
                        if (obj == null) {
1✔
UNCOV
66
                                return false;
×
67
                        }
68
                        if (getClass() != obj.getClass()) {
1✔
69
                                return false;
×
70
                        }
71
                        Key other = (Key) obj;
1✔
72
                        if (handlerType == null) {
1✔
73
                                if (other.handlerType != null) {
×
74
                                        return false;
×
75
                                }
76
                        } else if (!handlerType.equals(other.handlerType)) {
1✔
77
                                return false;
×
78
                        }
79
                        if (type == null) {
1✔
80
                                return other.type == null;
×
81
                        } else {
82
                                return type.equals(other.type);
1✔
83
                        }
84
                }
85
                
86
        }
87
        
88
        public static void clearCachedHandlers() {
89
                cachedHandlers = new WeakHashMap<>();
1✔
90
        }
1✔
91
        
92
        /**
93
         * Retrieves a List of all registered components from the Context that are of the passed
94
         * handlerType and one or more of the following is true:
95
         * <ul>
96
         * <li>The handlerType is annotated as a {@link Handler} that supports the passed type</li>
97
         * <li>The passed type is null - this effectively returns all components of the passed
98
         * handlerType</li>
99
         * </ul>
100
         * The returned handlers are ordered in the list based upon the order property.
101
         * 
102
         * @param handlerType Indicates the type of class to return
103
         * @param type Indicates the type that the given handlerType must support (or null for any)
104
         * @return a List of all matching Handlers for the given parameters, ordered by Handler#order
105
         * <strong>Should</strong> return a list of all classes that can handle the passed type
106
         * <strong>Should</strong> return classes registered in a module
107
         * <strong>Should</strong> return an empty list if no classes can handle the passed type
108
         */
109
        public static <H, T> List<H> getHandlersForType(Class<H> handlerType, Class<T> type) {
110
                List<?> list = cachedHandlers.get(new Key(handlerType, type));
1✔
111
                if (list != null) {
1✔
112
                        return (List<H>) list;
1✔
113
                }
114
                
115
                List<H> handlers = new ArrayList<>();
1✔
116
                
117
                // First get all registered components of the passed class
118
                log.debug("Getting handlers of type " + handlerType + (type == null ? "" : " for class " + type.getName()));
1✔
119
                for (H handler : Context.getRegisteredComponents(handlerType)) {
1✔
120
                        Handler handlerAnnotation = handler.getClass().getAnnotation(Handler.class);
1✔
121
                        // Only consider those that have been annotated as Handlers
122
                        if (handlerAnnotation != null) {
1✔
123
                                // If no type is passed in return all handlers
124
                                if (type == null) {
1✔
125
                                        log.debug("Found handler " + handler.getClass());
1✔
126
                                        handlers.add(handler);
1✔
127
                                }
128
                                // Otherwise, return all handlers that support the passed type
129
                                else {
130
                                        for (int i = 0; i < handlerAnnotation.supports().length; i++) {
1✔
131
                                                Class<?> clazz = handlerAnnotation.supports()[i];
1✔
132
                                                if (clazz.isAssignableFrom(type)) {
1✔
133
                                                        log.debug("Found handler: " + handler.getClass());
1✔
134
                                                        handlers.add(handler);
1✔
135
                                                }
136
                                        }
137
                                }
138
                        }
139
                }
1✔
140
                
141
                // Return the list of handlers based on the order specified in the Handler annotation
142
                handlers.sort(Comparator.comparing(o -> getOrderOfHandler(o.getClass())));
1✔
143
                
144
                Map<Key, List<?>> newCachedHandlers = new WeakHashMap<>(cachedHandlers);
1✔
145
                newCachedHandlers.put(new Key(handlerType, type), handlers);
1✔
146
                cachedHandlers = newCachedHandlers;
1✔
147
                
148
                return handlers;
1✔
149
        }
150
        
151
        /**
152
         * Retrieves the preferred Handler for a given handlerType and type. A <em>preferred</em>
153
         * handler is the Handler that has the lowest defined <em>order</em> attribute in it's
154
         * annotation. If multiple Handlers are found for the passed parameters at the lowest specified
155
         * order, then an APIException is thrown.
156
         * 
157
         * @param handlerType the class that is an annotated {@link Handler} to retrieve
158
         * @param type the class that the annotated {@link Handler} must support
159
         * @return the class of the passed handlerType with the lowest configured order
160
         * <strong>Should</strong> return the preferred handler for the passed handlerType and type
161
         * <strong>Should</strong> throw a APIException if no handler is found
162
         * <strong>Should</strong> throw a APIException if multiple preferred handlers are found
163
         * <strong>Should</strong> should return patient validator for patient
164
         * <strong>Should</strong> should return person validator for person
165
         */
166
        public static <H, T> H getPreferredHandler(Class<H> handlerType, Class<T> type) {
167
                
168
                if (handlerType == null || type == null) {
1✔
169
                        throw new IllegalArgumentException("You must specify both a handlerType and a type");
×
170
                }
171
                List<H> handlers = getHandlersForType(handlerType, type);
1✔
172
                if (handlers == null || handlers.isEmpty()) {
1✔
173
                        throw new APIException("handler.type.not.found", new Object[] { handlerType, type });
1✔
174
                }
175
                
176
                if (handlers.size() > 1) {
1✔
177
                        int order1 = getOrderOfHandler(handlers.get(0).getClass());
1✔
178
                        int order2 = getOrderOfHandler(handlers.get(1).getClass());
1✔
179
                        if (order1 == order2) {
1✔
180
                                throw new APIException("handler.type.multiple", new Object[] { handlerType, type });
×
181
                        }
182
                }
183
                
184
                return handlers.get(0);
1✔
185
        }
186
        
187
        /**
188
         * Utility method to return the order attribute of the {@link Handler} annotation on the passed
189
         * class. If the passed class does not have a {@link Handler} annotation, a RuntimeException is
190
         * thrown
191
         * 
192
         * @param handlerClass
193
         * @return the order attribute value
194
         */
195
        public static Integer getOrderOfHandler(Class<?> handlerClass) {
196
                Handler annotation = handlerClass.getAnnotation(Handler.class);
1✔
197
                if (annotation == null) {
1✔
198
                        throw new APIException("class.not.annotated.as.handler", new Object[] { handlerClass });
×
199
                }
200
                return annotation.order();
1✔
201
        }
202
        
203
        @Override
204
        public void onApplicationEvent(ContextRefreshedEvent event) {
205
                clearCachedHandlers();
1✔
206
        }
1✔
207
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc