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

openmrs / openmrs-core / 27824292656

19 Jun 2026 11:57AM UTC coverage: 63.653% (-0.08%) from 63.728%
27824292656

push

github

web-flow
TRUNK-6429: Addressing post commit review (#6190)

33 of 69 new or added lines in 10 files covered. (47.83%)

43 existing lines in 5 files now uncovered.

23926 of 37588 relevant lines covered (63.65%)

0.64 hits per line

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

67.27
/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.cache.Cache;
13
import com.google.common.cache.CacheBuilder;
14
import com.google.common.collect.MapMaker;
15
import org.slf4j.Logger;
16
import org.slf4j.LoggerFactory;
17
import org.springframework.beans.factory.SmartInitializingSingleton;
18
import org.springframework.beans.factory.annotation.Value;
19
import org.springframework.context.ApplicationContext;
20
import org.springframework.core.Ordered;
21
import org.springframework.core.ResolvableType;
22
import org.springframework.core.annotation.AnnotatedElementUtils;
23
import org.springframework.core.annotation.Order;
24
import org.springframework.stereotype.Component;
25
import org.springframework.util.ClassUtils;
26
import org.springframework.util.ReflectionUtils;
27

28
import java.lang.reflect.Method;
29
import java.time.Duration;
30
import java.util.ArrayList;
31
import java.util.Collections;
32
import java.util.List;
33
import java.util.Set;
34
import java.util.concurrent.ConcurrentMap;
35
import java.util.concurrent.ExecutionException;
36

37
/**
38
 * It is used to discover any {@link OutboxEventListener} listeners and
39
 * dispatch events to them.
40
 * 
41
 * @since 2.9.0
42
 */
43
@Component
44
public class OutboxEventRegistry implements SmartInitializingSingleton {
45

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

48
        private final ApplicationContext applicationContext;
49
        private final List<ListenerMethod> registry;
50
        private final Cache<ResolvableType, Boolean> hasOutboxListenersCache;
51
        private final boolean enabled;
52
        
53
        public OutboxEventRegistry(ApplicationContext applicationContext, @Value("${outboxevent.enabled:true}") boolean enabled) {
1✔
54
                this.applicationContext = applicationContext;
1✔
55
                this.registry = new ArrayList<>();
1✔
56
                this.hasOutboxListenersCache = CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(10)).build();
1✔
57
                this.enabled = enabled;
1✔
58
        }
1✔
59

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

85
                // Sort all listeners once at application startup
86
                Collections.sort(registry);
1✔
87
        }
1✔
88
        
89
        public boolean hasOutboxListeners() {
90
                return !registry.isEmpty();
1✔
91
        }
92
        
93

94
        public boolean hasOutboxListeners(Object event) {
95
                if (!hasOutboxListeners()) {
1✔
96
                        return false;
×
97
                }
98
                
99
                ResolvableType resolvableType =  ResolvableType.forInstance(event);
1✔
100

101
                try {
102
                        return hasOutboxListenersCache.get(resolvableType, () -> 
1✔
103
                                registry.stream().anyMatch(listener -> listener.supports(resolvableType))
1✔
104
                        );
NEW
105
                } catch (ExecutionException e) {
×
NEW
106
                        throw new RuntimeException(e);
×
107
                }
108

109
        }
110
        
111
        public void dispatchOutboxEvent(Object event, Set<String> completedListeners, Runnable listenerCallback) {
112
                ResolvableType resolvableType = ResolvableType.forInstance(event);
×
113
                
114
                for (ListenerMethod listener : registry) {
×
115
                        if (listener.supports(resolvableType) && !completedListeners.contains(listener.getListenerId())) {
×
116
                                listener.invoke(applicationContext, event);
×
117
                                completedListeners.add(listener.getListenerId());
×
118
                                listenerCallback.run();
×
119
                        }
120
                }
×
121
        }
×
122

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

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

138
                public boolean supports(ResolvableType eventType) {
139
                        return this.targetEventType.isAssignableFrom(eventType);
1✔
140
                }
141

142
                public String getListenerId() {
143
                        return listenerId;
×
144
                }
145

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

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