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

openmrs / openmrs-core / 26773559161

01 Jun 2026 06:21PM UTC coverage: 63.376% (-0.01%) from 63.389%
26773559161

push

github

web-flow
TRUNK-6429: Create application events for service method calls and entity changes (#6084)

272 of 504 new or added lines in 27 files covered. (53.97%)

5 existing lines in 2 files now uncovered.

23598 of 37235 relevant lines covered (63.38%)

0.63 hits per line

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

70.18
/api/src/main/java/org/openmrs/event/outbox/OutboxEventRegistry.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.event.outbox;
11

12
import com.google.common.collect.MapMaker;
13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15
import org.springframework.beans.factory.SmartInitializingSingleton;
16
import org.springframework.beans.factory.annotation.Value;
17
import org.springframework.context.ApplicationContext;
18
import org.springframework.core.Ordered;
19
import org.springframework.core.ResolvableType;
20
import org.springframework.core.annotation.AnnotatedElementUtils;
21
import org.springframework.core.annotation.Order;
22
import org.springframework.stereotype.Component;
23
import org.springframework.util.ClassUtils;
24
import org.springframework.util.ReflectionUtils;
25

26
import java.lang.reflect.Method;
27
import java.util.ArrayList;
28
import java.util.Collections;
29
import java.util.List;
30
import java.util.Set;
31
import java.util.concurrent.ConcurrentMap;
32

33
/**
34
 * It is used to discover any {@link OutboxEventListener} listeners and
35
 * dispatch events to them.
36
 * 
37
 * @since 2.9.x
38
 */
39
@Component
40
public class OutboxEventRegistry implements SmartInitializingSingleton {
41

42
        private static final Logger log = LoggerFactory.getLogger(OutboxEventRegistry.class);
1✔
43

44
        private final ApplicationContext applicationContext;
45
        private final List<ListenerMethod> registry;
46
        private final ConcurrentMap<ResolvableType, Boolean> hasOutboxListenersCache;
47
        private final boolean enabled;
48
        
49
        public OutboxEventRegistry(ApplicationContext applicationContext, @Value("${outboxevent.enabled:true}") boolean enabled) {
1✔
50
                this.applicationContext = applicationContext;
1✔
51
                this.registry = new ArrayList<>();
1✔
52
                this.hasOutboxListenersCache = new MapMaker().weakKeys().weakValues().makeMap();
1✔
53
                this.enabled = enabled;
1✔
54
        }
1✔
55

56
        @Override
57
        public void afterSingletonsInstantiated() {
58
                if (!enabled) {
1✔
NEW
59
                        return;
×
60
                }
61
                
62
                for (String beanName : applicationContext.getBeanDefinitionNames()) {
1✔
63
                        Class<?> type = applicationContext.getType(beanName);
1✔
64
                        if (type != null) {
1✔
65
                                // Extract original class just in case it is wrapped by CGLIB proxy (e.g., @Transactional beans)
66
                                Class<?> userClass = ClassUtils.getUserClass(type);
1✔
67
                                ReflectionUtils.doWithMethods(userClass, method -> {
1✔
68
                                        if (AnnotatedElementUtils.hasAnnotation(method, OutboxEventListener.class)) {
1✔
69
                                                Class<?>[] parameterTypes = method.getParameterTypes();
1✔
70
                                                if (parameterTypes.length == 1) {
1✔
71
                                                        ResolvableType eventType = ResolvableType.forMethodParameter(method, 0);
1✔
72
                                                        Order orderAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Order.class);
1✔
73
                                                        int order = orderAnnotation != null ? orderAnnotation.value() : Ordered.LOWEST_PRECEDENCE;
1✔
74
                                                        registry.add(new ListenerMethod(eventType, beanName, method, order));
1✔
75
                                                }
76
                                        }
77
                                });
1✔
78
                        }
79
                }
80

81
                // Sort all listeners once at application startup
82
                Collections.sort(registry);
1✔
83
        }
1✔
84
        
85
        public boolean hasOutboxListeners() {
86
                return !registry.isEmpty();
1✔
87
        }
88
        
89

90
        public boolean hasOutboxListeners(Object event) {
91
                if (!hasOutboxListeners()) {
1✔
NEW
92
                        return false;
×
93
                }
94
                
95
                ResolvableType resolvableType =  ResolvableType.forInstance(event);
1✔
96
                Boolean result = hasOutboxListenersCache.get(resolvableType);
1✔
97
                if (result != null) {
1✔
NEW
98
                        return result;
×
99
                }
100
                
101
                // Checks if the published event type (or its subclass) is registered for the outbox
102
                result = registry.stream().anyMatch(listener -> listener.supports(resolvableType));
1✔
103
                
104
                hasOutboxListenersCache.put(resolvableType, result);
1✔
105
                return result;
1✔
106
        }
107
        
108
        public void dispatchOutboxEvent(Object event, Set<String> completedListeners, Runnable listenerCallback) {
NEW
109
                ResolvableType resolvableType = ResolvableType.forInstance(event);
×
110
                
NEW
111
                for (ListenerMethod listener : registry) {
×
NEW
112
                        if (listener.supports(resolvableType) && !completedListeners.contains(listener.getListenerId())) {
×
NEW
113
                                listener.invoke(applicationContext, event);
×
NEW
114
                                completedListeners.add(listener.getListenerId());
×
NEW
115
                                listenerCallback.run();
×
116
                        }
NEW
117
                }
×
NEW
118
        }
×
119

120
        private static class ListenerMethod implements Comparable<ListenerMethod> {
121
                private final ResolvableType targetEventType;
122
                private final String beanName;
123
                private final Method method;
124
                private final int order;
125
                private final String listenerId;
126

127
                public ListenerMethod(ResolvableType targetEventType, String beanName, Method method, int order) {
1✔
128
                        this.targetEventType = targetEventType;
1✔
129
                        this.beanName = beanName;
1✔
130
                        this.method = method;
1✔
131
                        this.order = order;
1✔
132
                        this.listenerId = beanName + "." + method.getName();
1✔
133
                }
1✔
134

135
                public boolean supports(ResolvableType eventType) {
136
                        return this.targetEventType.isAssignableFrom(eventType);
1✔
137
                }
138

139
                public String getListenerId() {
NEW
140
                        return listenerId;
×
141
                }
142

143
                public void invoke(ApplicationContext context, Object event) {
NEW
144
                        Object bean = context.getBean(beanName);
×
145
                        // Ensure we invoke on the proxy if the bean is wrapped (e.g., @Transactional)
NEW
146
                        Method methodToInvoke = ClassUtils.getMostSpecificMethod(method, bean.getClass());
×
NEW
147
                        ReflectionUtils.makeAccessible(methodToInvoke);
×
NEW
148
                        ReflectionUtils.invokeMethod(methodToInvoke, bean, event);
×
NEW
149
                }
×
150

151
                @Override
152
                public int compareTo(ListenerMethod other) {
153
                        return Integer.compare(this.order, other.order);
1✔
154
                }
155
        }
156
}
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