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

openmrs / openmrs-core / 13015466563

28 Jan 2025 04:53PM UTC coverage: 63.705% (-0.1%) from 63.806%
13015466563

push

github

web-flow
TRUNK-6292: Changes to Liquibase files breaks OpenMRS startup (#4899)

21982 of 34506 relevant lines covered (63.7%)

0.64 hits per line

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

51.17
/api/src/main/java/org/openmrs/module/ModuleFactory.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.module;
11

12
import java.io.File;
13
import java.io.IOException;
14
import java.io.InputStream;
15
import java.net.MalformedURLException;
16
import java.net.URL;
17
import java.rmi.activation.Activator;
18
import java.util.ArrayList;
19
import java.util.Arrays;
20
import java.util.Collection;
21
import java.util.Collections;
22
import java.util.Comparator;
23
import java.util.HashMap;
24
import java.util.HashSet;
25
import java.util.LinkedHashSet;
26
import java.util.List;
27
import java.util.Map;
28
import java.util.Map.Entry;
29
import java.util.Set;
30
import java.util.SortedMap;
31
import java.util.WeakHashMap;
32
import java.util.concurrent.ConcurrentHashMap;
33

34
import org.aopalliance.aop.Advice;
35
import org.openmrs.GlobalProperty;
36
import org.openmrs.Privilege;
37
import org.openmrs.api.AdministrationService;
38
import org.openmrs.api.OpenmrsService;
39
import org.openmrs.api.context.Context;
40
import org.openmrs.api.context.Daemon;
41
import org.openmrs.module.Extension.MEDIA_TYPE;
42
import org.openmrs.util.CycleException;
43
import org.openmrs.util.DatabaseUpdater;
44
import org.openmrs.util.Graph;
45
import org.openmrs.util.InputRequiredException;
46
import org.openmrs.util.OpenmrsClassLoader;
47
import org.openmrs.util.OpenmrsConstants;
48
import org.openmrs.util.OpenmrsUtil;
49
import org.openmrs.util.PrivilegeConstants;
50
import org.slf4j.Logger;
51
import org.slf4j.LoggerFactory;
52
import org.springframework.aop.Advisor;
53
import org.springframework.context.support.AbstractRefreshableApplicationContext;
54
import org.springframework.util.StringUtils;
55

56
import liquibase.Contexts;
57

58
/**
59
 * Methods for loading, starting, stopping, and storing OpenMRS modules
60
 */
61
public class ModuleFactory {
62
        
63
        private ModuleFactory() {
64
        }
65
        
66
        private static final Logger log = LoggerFactory.getLogger(ModuleFactory.class);
1✔
67
        
68
        protected static volatile Map<String, Module> loadedModules = new WeakHashMap<>();
1✔
69
        
70
        protected static volatile Map<String, Module> startedModules = new WeakHashMap<>();
1✔
71
        
72
        protected static volatile Map<String, List<Extension>> extensionMap = new HashMap<>();
1✔
73
        
74
        // maps to keep track of the memory and objects to free/close
75
        protected static volatile Map<Module, ModuleClassLoader> moduleClassLoaders = new WeakHashMap<>();
1✔
76
        
77
        private static Map<String, Set<ModuleClassLoader>> providedPackages = new ConcurrentHashMap<>();
1✔
78
        
79
        // the name of the file within a module file
80
        private static final String MODULE_CHANGELOG_FILENAME = "liquibase.xml";
81
        
82
        private static final Map<String, DaemonToken> daemonTokens = new WeakHashMap<>();
1✔
83
        
84
        private static volatile Set<String> actualStartupOrder;
85
        
86
        /**
87
         * Add a module (in the form of a jar file) to the list of openmrs modules Returns null if an error
88
         * occurred and/or module was not successfully loaded
89
         * 
90
         * @param moduleFile
91
         * @return Module
92
         */
93
        public static Module loadModule(File moduleFile) throws ModuleException {
94
                
95
                return loadModule(moduleFile, true);
×
96
                
97
        }
98
        
99
        /**
100
         * Add a module (in the form of a jar file) to the list of openmrs modules Returns null if an error
101
         * occurred and/or module was not successfully loaded
102
         * 
103
         * @param moduleFile
104
         * @param replaceIfExists unload a module that has the same moduleId if one is loaded already
105
         * @return Module
106
         */
107
        public static Module loadModule(File moduleFile, Boolean replaceIfExists) throws ModuleException {
108
                Module module = new ModuleFileParser(Context.getMessageSourceService()).parse(moduleFile);
1✔
109
                
110
                if (module != null) {
1✔
111
                        loadModule(module, replaceIfExists);
1✔
112
                }
113
                
114
                return module;
1✔
115
        }
116
        
117
        /**
118
         * Add a module to the list of openmrs modules
119
         * 
120
         * @param module
121
         * @param replaceIfExists unload a module that has the same moduleId if one is loaded already
122
         *            <strong>Should</strong> load module if it is currently not loaded
123
         *            <strong>Should</strong> not load module if already loaded <strong>Should</strong>
124
         *            always load module if replacement is wanted <strong>Should</strong> not load an older
125
         *            version of the same module <strong>Should</strong> load a newer version of the same
126
         *            module
127
         * @return module the module that was loaded or if the module exists already with the same version,
128
         *         the old module
129
         */
130
        public static Module loadModule(Module module, Boolean replaceIfExists) throws ModuleException {
131
                
132
                log.debug("Adding module {} to the module queue", module.getName());
1✔
133
                
134
                Module oldModule = getLoadedModulesMap().get(module.getModuleId());
1✔
135
                if (oldModule != null) {
1✔
136
                        int versionComparison = ModuleUtil.compareVersion(oldModule.getVersion(), module.getVersion());
1✔
137
                        if (versionComparison < 0) {
1✔
138
                                // if oldModule version is lower, unload it and use the new
139
                                unloadModule(oldModule);
1✔
140
                        } else if (versionComparison == 0) {
1✔
141
                                if (replaceIfExists) {
1✔
142
                                        // if the versions are the same and we're told to replaceIfExists, use the new
143
                                        unloadModule(oldModule);
1✔
144
                                } else {
145
                                        // if the versions are equal and we're not told to replaceIfExists, jump out of here in a bad way
146
                                        throw new ModuleException("A module with the same id and version already exists", module.getModuleId());
1✔
147
                                }
148
                        } else {
149
                                // if the older (already loaded) module is newer, keep that original one that was loaded. return that one.
150
                                return oldModule;
1✔
151
                        }
152
                }
153
                
154
                getLoadedModulesMap().put(module.getModuleId(), module);
1✔
155
                
156
                return module;
1✔
157
        }
158
        
159
        /**
160
         * Load OpenMRS modules from <code>OpenmrsUtil.getModuleRepository()</code>
161
         */
162
        public static void loadModules() {
163
                
164
                // load modules from the user's module repository directory
165
                File modulesFolder = ModuleUtil.getModuleRepository();
×
166
                
167
                log.debug("Loading modules from: {}", modulesFolder.getAbsolutePath());
×
168
                
169
                File[] files = modulesFolder.listFiles();
×
170
                if (modulesFolder.isDirectory() && files != null) {
×
171
                        loadModules(Arrays.asList(files));
×
172
                } else {
173
                        log.error("modules folder: '" + modulesFolder.getAbsolutePath() + "' is not a directory or IO error occurred");
×
174
                }
175
        }
×
176
        
177
        /**
178
         * Attempt to load the given files as OpenMRS modules
179
         * 
180
         * @param modulesToLoad the list of files to try and load <strong>Should</strong> not crash when
181
         *            file is not found or broken <strong>Should</strong> setup requirement mappings for
182
         *            every module <strong>Should</strong> not start the loaded modules
183
         */
184
        public static void loadModules(List<File> modulesToLoad) {
185
                // loop over the modules and load all the modules that we can
186
                for (File f : modulesToLoad) {
1✔
187
                        if (f.exists()) {
1✔
188
                                // ignore .svn folder and the like
189
                                if (!f.getName().startsWith(".")) {
1✔
190
                                        try {
191
                                                // last module loaded wins
192
                                                Module mod = loadModule(f, true);
1✔
193
                                                log.debug("Loaded module: " + mod + " successfully");
1✔
194
                                        }
195
                                        catch (Exception e) {
×
196
                                                log.error("Unable to load file in module directory: " + f + ". Skipping file.", e);
×
197
                                        }
1✔
198
                                }
199
                        } else {
200
                                log.error("Could not find file in module directory: " + f);
1✔
201
                        }
202
                }
1✔
203
                
204
                //inform modules, that they can't start before other modules
205
                
206
                Map<String, Module> loadedModulesMap = getLoadedModulesMapPackage();
1✔
207
                for (Module m : loadedModulesMap.values()) {
1✔
208
                        Map<String, String> startBeforeModules = m.getStartBeforeModulesMap();
1✔
209
                        if (startBeforeModules.size() > 0) {
1✔
210
                                for (String s : startBeforeModules.keySet()) {
1✔
211
                                        Module mod = loadedModulesMap.get(s);
1✔
212
                                        if (mod != null) {
1✔
213
                                                mod.addRequiredModule(m.getPackageName(), m.getVersion());
1✔
214
                                        }
215
                                }
1✔
216
                        }
217
                }
1✔
218
        }
1✔
219
        
220
        /**
221
         * Try to start all of the loaded modules that have the global property <i>moduleId</i>.started is
222
         * set to "true" or the property does not exist. Otherwise, leave it as only "loaded"<br>
223
         * <br>
224
         * Modules that are already started will be skipped.
225
         */
226
        public static void startModules() {
227
                
228
                // loop over and try starting each of the loaded modules
229
                if (!getLoadedModules().isEmpty()) {
1✔
230
                        
231
                        List<Module> modules = getModulesThatShouldStart();
1✔
232
                        
233
                        try {
234
                                modules = getModulesInStartupOrder(modules);
1✔
235
                        }
236
                        catch (CycleException ex) {
×
237
                                String message = getCyclicDependenciesMessage(ex.getMessage());
×
238
                                log.error(message, ex);
×
239
                                notifySuperUsersAboutCyclicDependencies(ex);
×
240
                                modules = (List<Module>) ex.getExtraData();
×
241
                        }
1✔
242
                        
243
                        // try and start the modules that should be started
244
                        for (Module mod : modules) {
1✔
245
                                
246
                                if (mod.isStarted()) {
1✔
247
                                        // skip over modules that are already started
248
                                        continue;
×
249
                                }
250
                                
251
                                // Skip module if required ones are not started
252
                                if (!requiredModulesStarted(mod)) {
1✔
253
                                        String message = getFailedToStartModuleMessage(mod);
×
254
                                        log.error(message);
×
255
                                        mod.setStartupErrorMessage(message);
×
256
                                        notifySuperUsersAboutModuleFailure(mod);
×
257
                                        continue;
×
258
                                }
259
                                
260
                                try {
261
                                        log.debug("starting module: {}", mod.getModuleId());
1✔
262
                                        startModule(mod);
1✔
263
                                }
264
                                catch (Exception e) {
×
265
                                        log.error("Error while starting module: " + mod.getName(), e);
×
266
                                        mod.setStartupErrorMessage("Error while starting module", e);
×
267
                                        notifySuperUsersAboutModuleFailure(mod);
×
268
                                }
1✔
269
                        }
1✔
270
                }
271
        }
1✔
272
        
273
        /**
274
         * Obtain the list of modules that should be started
275
         * 
276
         * @return list of modules
277
         */
278
        private static List<Module> getModulesThatShouldStart() {
279
                List<Module> modules = new ArrayList<>();
1✔
280
                
281
                AdministrationService adminService = Context.getAdministrationService();
1✔
282
                
283
                for (Module mod : getLoadedModulesCoreFirst()) {
1✔
284
                        
285
                        String key = mod.getModuleId() + ".started";
1✔
286
                        String startedProp = adminService.getGlobalProperty(key, null);
1✔
287
                        String mandatoryProp = adminService.getGlobalProperty(mod.getModuleId() + ".mandatory", null);
1✔
288
                        
289
                        boolean isCoreToOpenmrs = mod.isCore() && !ModuleUtil.ignoreCoreModules();
1✔
290
                        
291
                        // if a 'moduleid.started' property doesn't exist, start the module anyway
292
                        // as this is probably the first time they are loading it
293
                        if (startedProp == null || "true".equals(startedProp) || "true".equalsIgnoreCase(mandatoryProp)
1✔
294
                                || mod.isMandatory() || isCoreToOpenmrs) {
×
295
                                modules.add(mod);
1✔
296
                        }
297
                }
1✔
298
                return modules;
1✔
299
        }
300
        
301
        /**
302
         * Sort modules in startup order based on required and aware-of dependencies
303
         * 
304
         * @param modules list of modules to sort
305
         * @return list of modules sorted by dependencies
306
         * @throws CycleException
307
         */
308
        public static List<Module> getModulesInStartupOrder(Collection<Module> modules) throws CycleException {
309
                Graph<Module> graph = new Graph<>();
1✔
310
                
311
                for (Module mod : modules) {
1✔
312
                        
313
                        graph.addNode(mod);
1✔
314
                        
315
                        // Required dependencies
316
                        for (String key : mod.getRequiredModules()) {
1✔
317
                                Module module = getModuleByPackage(key);
1✔
318
                                Module fromNode = graph.getNode(module);
1✔
319
                                if (fromNode == null) {
1✔
320
                                        fromNode = module;
1✔
321
                                }
322
                                
323
                                if (fromNode != null) {
1✔
324
                                        graph.addEdge(graph.new Edge(
1✔
325
                                                                     fromNode,
326
                                                                     mod));
327
                                }
328
                        }
1✔
329
                        
330
                        // Aware-of dependencies
331
                        for (String key : mod.getAwareOfModules()) {
1✔
332
                                Module module = getModuleByPackage(key);
1✔
333
                                Module fromNode = graph.getNode(module);
1✔
334
                                if (fromNode == null) {
1✔
335
                                        fromNode = module;
1✔
336
                                }
337
                                
338
                                if (fromNode != null) {
1✔
339
                                        graph.addEdge(graph.new Edge(
×
340
                                                                     fromNode,
341
                                                                     mod));
342
                                }
343
                        }
1✔
344
                }
1✔
345
                
346
                return graph.topologicalSort();
1✔
347
        }
348
        
349
        /**
350
         * Send an Alert to all super users that the given module did not start successfully.
351
         * 
352
         * @param mod The Module that failed
353
         */
354
        private static void notifySuperUsersAboutModuleFailure(Module mod) {
355
                try {
356
                        // Add the privileges necessary for notifySuperUsers
357
                        Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
358
                        
359
                        // Send an alert to all administrators
360
                        Context.getAlertService().notifySuperUsers("Module.startupError.notification.message", null, mod.getName());
×
361
                }
362
                catch (Exception e) {
×
363
                        log.error("Unable to send an alert to the super users", e);
×
364
                }
365
                finally {
366
                        // Remove added privileges
367
                        Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
368
                }
369
        }
×
370
        
371
        /**
372
         * Send an Alert to all super users that modules did not start due to cyclic dependencies
373
         */
374
        private static void notifySuperUsersAboutCyclicDependencies(Exception ex) {
375
                try {
376
                        Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
377
                        Context.getAlertService().notifySuperUsers("Module.error.cyclicDependencies", ex, ex.getMessage());
×
378
                }
379
                catch (Exception e) {
×
380
                        log.error("Unable to send an alert to the super users", e);
×
381
                }
382
                finally {
383
                        Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
384
                }
385
        }
×
386
        
387
        /**
388
         * Returns all modules found/loaded into the system (started and not started), with the core modules
389
         * at the start of that list
390
         * 
391
         * @return <code>List&lt;Module&gt;</code> of the modules loaded into the system, with the core
392
         *         modules first.
393
         */
394
        public static List<Module> getLoadedModulesCoreFirst() {
395
                List<Module> list = new ArrayList<>(getLoadedModules());
1✔
396
                final Collection<String> coreModuleIds = ModuleConstants.CORE_MODULES.keySet();
1✔
397
                list.sort((left, right) -> {
1✔
398
                        Integer leftVal = coreModuleIds.contains(left.getModuleId()) ? 0 : 1;
1✔
399
                        Integer rightVal = coreModuleIds.contains(right.getModuleId()) ? 0 : 1;
1✔
400
                        return leftVal.compareTo(rightVal);
1✔
401
                });
402
                return list;
1✔
403
        }
404
        
405
        /**
406
         * Convenience method to return a List of Strings containing a description of which modules the
407
         * passed module requires but which are not started. The returned description of each module is the
408
         * moduleId followed by the required version if one is specified
409
         * 
410
         * @param module the module to check required modules for
411
         * @return List&lt;String&gt; of module names + optional required versions: "org.openmrs.formentry
412
         *         1.8, org.rg.patientmatching"
413
         */
414
        private static List<String> getMissingRequiredModules(Module module) {
415
                List<String> ret = new ArrayList<>();
×
416
                for (String moduleName : module.getRequiredModules()) {
×
417
                        boolean started = false;
×
418
                        for (Module mod : getStartedModules()) {
×
419
                                if (mod.getPackageName().equals(moduleName)) {
×
420
                                        String reqVersion = module.getRequiredModuleVersion(moduleName);
×
421
                                        if (reqVersion == null || ModuleUtil.compareVersion(mod.getVersion(), reqVersion) >= 0) {
×
422
                                                started = true;
×
423
                                        }
424
                                        break;
425
                                }
426
                        }
×
427
                        
428
                        if (!started) {
×
429
                                String moduleVersion = module.getRequiredModuleVersion(moduleName);
×
430
                                moduleName = moduleName.replace("org.openmrs.module.", "").replace("org.openmrs.", "");
×
431
                                ret.add(moduleName + (moduleVersion != null ? " " + moduleVersion : ""));
×
432
                        }
433
                }
×
434
                return ret;
×
435
        }
436
        
437
        /**
438
         * Returns all modules found/loaded into the system (started and not started)
439
         * 
440
         * @return <code>Collection&lt;Module&gt;</code> of the modules loaded into the system
441
         */
442
        public static Collection<Module> getLoadedModules() {
443
                if (getLoadedModulesMap().size() > 0) {
1✔
444
                        return getLoadedModulesMap().values();
1✔
445
                }
446
                
447
                return Collections.emptyList();
1✔
448
        }
449
        
450
        /**
451
         * Returns all modules found/loaded into the system (started and not started) in the form of a
452
         * map&lt;ModuleId, Module&gt;
453
         * 
454
         * @return map&lt;ModuleId, Module&gt;
455
         */
456
        public static Map<String, Module> getLoadedModulesMap() {
457
                if (loadedModules == null) {
1✔
458
                        loadedModules = new WeakHashMap<>();
1✔
459
                }
460
                
461
                return loadedModules;
1✔
462
        }
463
        
464
        /**
465
         * Returns all modules found/loaded into the system (started and not started) in the form of a
466
         * map&lt;PackageName, Module&gt;
467
         * 
468
         * @return map&lt;PackageName, Module&gt;
469
         */
470
        public static Map<String, Module> getLoadedModulesMapPackage() {
471
                if (loadedModules == null) {
1✔
472
                        loadedModules = new WeakHashMap<>();
×
473
                        return loadedModules;
×
474
                }
475
                
476
                Map<String, Module> map = new WeakHashMap<>();
1✔
477
                for (Module loadedModule : loadedModules.values()) {
1✔
478
                        map.put(loadedModule.getPackageName(), loadedModule);
1✔
479
                }
1✔
480
                return map;
1✔
481
        }
482
        
483
        /**
484
         * Returns the modules that have been successfully started
485
         * 
486
         * @return <code>Collection&lt;Module&gt;</code> of the started modules
487
         */
488
        public static Collection<Module> getStartedModules() {
489
                if (getStartedModulesMap().size() > 0) {
1✔
490
                        return getStartedModulesMap().values();
1✔
491
                }
492
                
493
                return Collections.emptyList();
1✔
494
        }
495
        
496
        public static List<Module> getStartedModulesInOrder() {
497
                List<Module> modules = new ArrayList<>();
×
498
                if (actualStartupOrder != null) {
×
499
                        for (String moduleId : actualStartupOrder) {
×
500
                                modules.add(getStartedModulesMap().get(moduleId));
×
501
                        }
×
502
                } else {
503
                        modules.addAll(getStartedModules());
×
504
                }
505
                return modules;
×
506
        }
507
        
508
        /**
509
         * Returns the modules that have been successfully started in the form of a map&lt;ModuleId,
510
         * Module&gt;
511
         * 
512
         * @return Map&lt;ModuleId, Module&gt;
513
         */
514
        public static Map<String, Module> getStartedModulesMap() {
515
                if (startedModules == null) {
1✔
516
                        startedModules = new WeakHashMap<>();
1✔
517
                }
518
                
519
                return startedModules;
1✔
520
        }
521
        
522
        /**
523
         * @param moduleId
524
         * @return Module matching module id or null if none
525
         */
526
        public static Module getModuleById(String moduleId) {
527
                return getLoadedModulesMap().get(moduleId);
1✔
528
        }
529
        
530
        /**
531
         * @param moduleId
532
         * @return Module matching moduleId, if it is started or null otherwise
533
         */
534
        public static Module getStartedModuleById(String moduleId) {
535
                return getStartedModulesMap().get(moduleId);
1✔
536
        }
537
        
538
        /**
539
         * @param modulePackage
540
         * @return Module matching module package or null if none
541
         */
542
        public static Module getModuleByPackage(String modulePackage) {
543
                for (Module mod : getLoadedModulesMap().values()) {
1✔
544
                        if (mod.getPackageName().equals(modulePackage)) {
1✔
545
                                return mod;
1✔
546
                        }
547
                }
1✔
548
                return null;
1✔
549
        }
550
        
551
        /**
552
         * @see #startModule(Module, boolean, AbstractRefreshableApplicationContext)
553
         * @see #startModuleInternal(Module)
554
         * @see Daemon#startModule(Module)
555
         */
556
        public static Module startModule(Module module) throws ModuleException {
557
                return startModule(module, false, null);
1✔
558
        }
559
        
560
        /**
561
         * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
562
         * Module's activator. This method is run in a new thread and is authenticated as the Daemon user.
563
         * If a non null application context is passed in, it gets refreshed to make the module's services
564
         * available
565
         * 
566
         * @param module Module to start
567
         * @param isOpenmrsStartup Specifies whether this module is being started at application startup or
568
         *            not, this argument is ignored if a null application context is passed in
569
         * @param applicationContext the spring application context instance to refresh
570
         * @throws ModuleException if the module throws any kind of error at startup or in an activator
571
         * @see #startModuleInternal(Module, boolean, AbstractRefreshableApplicationContext)
572
         * @see Daemon#startModule(Module, boolean, AbstractRefreshableApplicationContext)
573
         */
574
        public static Module startModule(Module module, boolean isOpenmrsStartup,
575
                AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
576
                
577
                if (!requiredModulesStarted(module)) {
1✔
578
                        int missingModules = 0;
1✔
579
                        
580
                        for (String packageName : module.getRequiredModulesMap().keySet()) {
1✔
581
                                Module mod = getModuleByPackage(packageName);
1✔
582
                                
583
                                // mod not installed
584
                                if (mod == null) {
1✔
585
                                        missingModules++;
×
586
                                        continue;
×
587
                                }
588
                                
589
                                if (!mod.isStarted()) {
1✔
590
                                        startModule(mod);
1✔
591
                                }
592
                        }
1✔
593
                        
594
                        if (missingModules > 0) {
1✔
595
                                String message = getFailedToStartModuleMessage(module);
×
596
                                log.error(message);
×
597
                                module.setStartupErrorMessage(message);
×
598
                                notifySuperUsersAboutModuleFailure(module);
×
599
                                // instead of return null, i realized that Daemon.startModule() always returns a Module
600
                                // object,irrespective of whether the startup succeeded
601
                                return module;
×
602
                        }
603
                }
604
                return Daemon.startModule(module, isOpenmrsStartup, applicationContext);
1✔
605
        }
606
        
607
        /**
608
         * This method should not be called directly.<br>
609
         * <br>
610
         * The {@link #startModule(Module)} (and hence {@link Daemon#startModule(Module)}) calls this method
611
         * in a new Thread and is authenticated as the {@link Daemon} user<br>
612
         * <br>
613
         * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
614
         * Module's activator.
615
         * 
616
         * @param module Module to start
617
         */
618
        public static Module startModuleInternal(Module module) throws ModuleException {
619
                return startModuleInternal(module, false, null);
×
620
        }
621
        
622
        /**
623
         * This method should not be called directly.<br>
624
         * <br>
625
         * The {@link #startModule(Module)} (and hence {@link Daemon#startModule(Module)}) calls this method
626
         * in a new Thread and is authenticated as the {@link Daemon} user<br>
627
         * <br>
628
         * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
629
         * Module's activator. <br>
630
         * <br>
631
         * If a non null application context is passed in, it gets refreshed to make the module's services
632
         * available
633
         * 
634
         * @param module Module to start
635
         * @param isOpenmrsStartup Specifies whether this module is being started at application startup or
636
         *            not, this argument is ignored if a null application context is passed in
637
         * @param applicationContext the spring application context instance to refresh
638
         */
639
        public static Module startModuleInternal(Module module, boolean isOpenmrsStartup,
640
                AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
641
                
642
                if (module != null) {
1✔
643
                        String moduleId = module.getModuleId();
1✔
644
                        
645
                        try {
646
                                
647
                                // check to be sure this module can run with our current version
648
                                // of OpenMRS code
649
                                String requireVersion = module.getRequireOpenmrsVersion();
1✔
650
                                ModuleUtil.checkRequiredVersion(OpenmrsConstants.OPENMRS_VERSION_SHORT, requireVersion);
1✔
651
                                
652
                                // check for required modules
653
                                if (!requiredModulesStarted(module)) {
1✔
654
                                        throw new ModuleException(getFailedToStartModuleMessage(module));
×
655
                                }
656
                                
657
                                // fire up the classloader for this module
658
                                ModuleClassLoader moduleClassLoader = new ModuleClassLoader(module, ModuleFactory.class.getClassLoader());
1✔
659
                                getModuleClassLoaderMap().put(module, moduleClassLoader);
1✔
660
                                registerProvidedPackages(moduleClassLoader);
1✔
661
                                
662
                                // don't load the advice objects into the Context
663
                                // At startup, the spring context isn't refreshed until all modules
664
                                // have been loaded.  This causes errors if called here during a
665
                                // module's startup if one of these advice points is on another
666
                                // module because that other module's service won't have been loaded
667
                                // into spring yet.  All advice for all modules must be reloaded
668
                                // a spring context refresh anyway
669
                                
670
                                // map extension point to a list of extensions for this module only
671
                                Map<String, List<Extension>> moduleExtensionMap = new HashMap<>();
1✔
672
                                for (Extension ext : module.getExtensions()) {
1✔
673
                                        
674
                                        String extId = ext.getExtensionId();
×
675
                                        List<Extension> tmpExtensions = moduleExtensionMap.computeIfAbsent(extId, k -> new ArrayList<>());
×
676
                                        
677
                                        tmpExtensions.add(ext);
×
678
                                }
×
679
                                
680
                                // Sort this module's extensions, and merge them into the full extensions map
681
                                Comparator<Extension> sortOrder = (e1, e2) -> Integer.valueOf(e1.getOrder()).compareTo(e2.getOrder());
1✔
682
                                for (Map.Entry<String, List<Extension>> moduleExtensionEntry : moduleExtensionMap.entrySet()) {
1✔
683
                                        // Sort this module's extensions for current extension point
684
                                        List<Extension> sortedModuleExtensions = moduleExtensionEntry.getValue();
×
685
                                        sortedModuleExtensions.sort(sortOrder);
×
686
                                        
687
                                        // Get existing extensions, and append the ones from the new module
688
                                        List<Extension> extensions = getExtensionMap().computeIfAbsent(moduleExtensionEntry.getKey(),
×
689
                                            k -> new ArrayList<>());
×
690
                                        for (Extension ext : sortedModuleExtensions) {
×
691
                                                log.debug("Adding to mapping ext: " + ext.getExtensionId() + " ext.class: " + ext.getClass());
×
692
                                                extensions.add(ext);
×
693
                                        }
×
694
                                }
×
695
                                
696
                                // run the module's sql update script
697
                                // This and the property updates are the only things that can't
698
                                // be undone at startup, so put these calls after any other
699
                                // calls that might hinder startup
700
                                SortedMap<String, String> diffs = SqlDiffFileParser.getSqlDiffs(module);
1✔
701
                                
702
                                try {
703
                                        // this method must check and run queries against the database.
704
                                        // to do this, it must be "authenticated".  Give the current
705
                                        // "user" the proxy privilege so this can be done. ("user" might
706
                                        // be nobody because this is being run at startup)
707
                                        Context.addProxyPrivilege("");
1✔
708
                                        
709
                                        for (Map.Entry<String, String> entry : diffs.entrySet()) {
1✔
710
                                                String version = entry.getKey();
×
711
                                                String sql = entry.getValue();
×
712
                                                if (StringUtils.hasText(sql)) {
×
713
                                                        runDiff(module, version, sql);
×
714
                                                }
715
                                        }
×
716
                                }
717
                                finally {
718
                                        // take the "authenticated" privilege away from the current "user"
719
                                        Context.removeProxyPrivilege("");
1✔
720
                                }
721
                                
722
                                // run module's optional liquibase.xml immediately after sqldiff.xml
723
                                runLiquibase(module);
1✔
724
                                
725
                                // effectively mark this module as started successfully
726
                                getStartedModulesMap().put(moduleId, module);
1✔
727
                                if (actualStartupOrder == null) {
1✔
728
                                        actualStartupOrder = new LinkedHashSet<>();
1✔
729
                                }
730
                                actualStartupOrder.add(moduleId);
1✔
731
                                
732
                                try {
733
                                        // save the state of this module for future restarts
734
                                        saveGlobalProperty(moduleId + ".started", "true", getGlobalPropertyStartedDescription(moduleId));
1✔
735
                                        
736
                                        // save the mandatory status
737
                                        saveGlobalProperty(moduleId + ".mandatory", String.valueOf(module.isMandatory()),
1✔
738
                                            getGlobalPropertyMandatoryModuleDescription(moduleId));
1✔
739
                                }
740
                                catch (Exception e) {
×
741
                                        // pass over errors because this doesn't really concern startup
742
                                        // passing over this also allows for multiple of the same-named modules
743
                                        // to be loaded in junit tests that are run within one session
744
                                        log.debug("Got an error when trying to set the global property on module startup", e);
×
745
                                }
1✔
746
                                
747
                                // (this must be done after putting the module in the started
748
                                // list)
749
                                // if this module defined any privileges or global properties,
750
                                // make sure they are added to the database
751
                                // (Unfortunately, placing the call here will duplicate work
752
                                // done at initial app startup)
753
                                if (!module.getPrivileges().isEmpty() || !module.getGlobalProperties().isEmpty()) {
1✔
754
                                        log.debug("Updating core dataset");
×
755
                                        Context.checkCoreDataset();
×
756
                                        // checkCoreDataset() currently doesn't throw an error. If
757
                                        // it did, it needs to be
758
                                        // caught and the module needs to be stopped and given a
759
                                        // startup error
760
                                }
761
                                
762
                                // should be near the bottom so the module has all of its stuff
763
                                // set up for it already.
764
                                try {
765
                                        if (module.getModuleActivator() != null) {
1✔
766
                                                // if extends BaseModuleActivator
767
                                                module.getModuleActivator().willStart();
1✔
768
                                        }
769
                                }
770
                                catch (ModuleException e) {
×
771
                                        // just rethrow module exceptions. This should be used for a
772
                                        // module marking that it had trouble starting
773
                                        throw e;
×
774
                                }
775
                                catch (Exception e) {
×
776
                                        throw new ModuleException("Error while calling module's Activator.startup()/willStart() method", e);
×
777
                                }
1✔
778
                                
779
                                // erase any previous startup error
780
                                module.clearStartupError();
1✔
781
                        }
782
                        catch (Exception e) {
×
783
                                log.warn("Error while trying to start module: " + moduleId, e);
×
784
                                module.setStartupErrorMessage("Error while trying to start module", e);
×
785
                                notifySuperUsersAboutModuleFailure(module);
×
786
                                // undo all of the actions in startup
787
                                try {
788
                                        boolean skipOverStartedProperty = false;
×
789
                                        
790
                                        if (e instanceof ModuleMustStartException) {
×
791
                                                skipOverStartedProperty = true;
×
792
                                        }
793
                                        
794
                                        stopModule(module, skipOverStartedProperty, true);
×
795
                                }
796
                                catch (Exception e2) {
×
797
                                        // this will probably occur about the same place as the
798
                                        // error in startup
799
                                        log.debug("Error while stopping module: " + moduleId, e2);
×
800
                                }
×
801
                        }
1✔
802
                        
803
                }
804
                
805
                if (applicationContext != null) {
1✔
806
                        ModuleUtil.refreshApplicationContext(applicationContext, isOpenmrsStartup, module);
×
807
                }
808
                
809
                return module;
1✔
810
        }
811
        
812
        private static void registerProvidedPackages(ModuleClassLoader moduleClassLoader) {
813
                for (String providedPackage : moduleClassLoader.getProvidedPackages()) {
1✔
814
                        Set<ModuleClassLoader> newSet = new HashSet<>();
1✔
815
                        
816
                        Set<ModuleClassLoader> set = providedPackages.get(providedPackage);
1✔
817
                        if (set != null) {
1✔
818
                                newSet.addAll(set);
1✔
819
                        }
820
                        
821
                        newSet.add(moduleClassLoader);
1✔
822
                        providedPackages.put(providedPackage, newSet);
1✔
823
                }
1✔
824
        }
1✔
825
        
826
        private static void unregisterProvidedPackages(ModuleClassLoader moduleClassLoader) {
827
                for (String providedPackage : moduleClassLoader.getProvidedPackages()) {
1✔
828
                        Set<ModuleClassLoader> newSet = new HashSet<>();
1✔
829
                        
830
                        Set<ModuleClassLoader> set = providedPackages.get(providedPackage);
1✔
831
                        if (set != null) {
1✔
832
                                newSet.addAll(set);
1✔
833
                        }
834
                        newSet.remove(moduleClassLoader);
1✔
835
                        
836
                        providedPackages.put(providedPackage, newSet);
1✔
837
                }
1✔
838
        }
1✔
839
        
840
        public static Set<ModuleClassLoader> getModuleClassLoadersForPackage(String packageName) {
841
                Set<ModuleClassLoader> set = providedPackages.get(packageName);
1✔
842
                if (set == null) {
1✔
843
                        return Collections.emptySet();
1✔
844
                } else {
845
                        return new HashSet<>(set);
1✔
846
                }
847
        }
848
        
849
        /**
850
         * Gets the error message of a module which fails to start.
851
         * 
852
         * @param module the module that has failed to start.
853
         * @return the message text.
854
         */
855
        private static String getFailedToStartModuleMessage(Module module) {
856
                String[] params = { module.getName(), String.join(",", getMissingRequiredModules(module)) };
×
857
                return Context.getMessageSourceService().getMessage("Module.error.moduleCannotBeStarted", params,
×
858
                    Context.getLocale());
×
859
        }
860
        
861
        /**
862
         * Gets the error message of cyclic dependencies between modules
863
         * 
864
         * @return the message text.
865
         */
866
        private static String getCyclicDependenciesMessage(String message) {
867
                return Context.getMessageSourceService().getMessage("Module.error.cyclicDependencies", new Object[] { message },
×
868
                    Context.getLocale());
×
869
        }
870
        
871
        /**
872
         * Loop over the given module's advice objects and load them into the Context This needs to be
873
         * called for all started modules after every restart of the Spring Application Context
874
         * 
875
         * @param module
876
         */
877
        public static void loadAdvice(Module module) {
878
                for (AdvicePoint advice : module.getAdvicePoints()) {
×
879
                        Class<?> cls;
880
                        try {
881
                                cls = Context.loadClass(advice.getPoint());
×
882
                                Object aopObject = advice.getClassInstance();
×
883
                                if (aopObject instanceof Advisor) {
×
884
                                        log.debug("adding advisor [{}]", aopObject.getClass());
×
885
                                        Context.addAdvisor(cls, (Advisor) aopObject);
×
886
                                } else if (aopObject != null) {
×
887
                                        log.debug("adding advice [{}]", aopObject.getClass());
×
888
                                        Context.addAdvice(cls, (Advice) aopObject);
×
889
                                } else {
890
                                        log.debug("Could not load advice class for {} [{}]", advice.getPoint(), advice.getClassName());
×
891
                                }
892
                        }
893
                        catch (ClassNotFoundException | NoClassDefFoundError e) {
×
894
                                log.warn("Could not load advice point [{}]", advice.getPoint(), e);
×
895
                        }
×
896
                }
×
897
        }
×
898
        
899
        /**
900
         * Execute the given sql diff section for the given module
901
         * 
902
         * @param module the module being executed on
903
         * @param version the version of this sql diff
904
         * @param sql the actual sql statements to run (separated by semi colons)
905
         */
906
        private static void runDiff(Module module, String version, String sql) {
907
                AdministrationService as = Context.getAdministrationService();
×
908
                
909
                String key = module.getModuleId() + ".database_version";
×
910
                GlobalProperty gp = as.getGlobalPropertyObject(key);
×
911
                
912
                boolean executeSQL = false;
×
913
                
914
                // check given version against current version
915
                if (gp != null && StringUtils.hasLength(gp.getPropertyValue())) {
×
916
                        String currentDbVersion = gp.getPropertyValue();
×
917
                        if (log.isDebugEnabled()) {
×
918
                                log.debug("version:column {}:{}", version, currentDbVersion);
×
919
                                log.debug("compare: {}", ModuleUtil.compareVersion(version, currentDbVersion));
×
920
                        }
921
                        if (ModuleUtil.compareVersion(version, currentDbVersion) > 0) {
×
922
                                executeSQL = true;
×
923
                        }
924
                } else {
×
925
                        executeSQL = true;
×
926
                }
927
                
928
                // version is greater than the currently installed version. execute this update.
929
                if (executeSQL) {
×
930
                        try {
931
                                Context.addProxyPrivilege(PrivilegeConstants.SQL_LEVEL_ACCESS);
×
932
                                log.debug("Executing sql: " + sql);
×
933
                                String[] sqlStatements = sql.split(";");
×
934
                                for (String sqlStatement : sqlStatements) {
×
935
                                        if (sqlStatement.trim().length() > 0) {
×
936
                                                as.executeSQL(sqlStatement, false);
×
937
                                        }
938
                                }
939
                        }
940
                        finally {
941
                                Context.removeProxyPrivilege(PrivilegeConstants.SQL_LEVEL_ACCESS);
×
942
                        }
943
                        
944
                        // save the global property
945
                        try {
946
                                Context.addProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);
×
947
                                
948
                                String description = "DO NOT MODIFY.  Current database version number for the " + module.getModuleId()
×
949
                                        + " module.";
950
                                
951
                                if (gp == null) {
×
952
                                        log.info("Global property " + key + " was not found. Creating one now.");
×
953
                                        gp = new GlobalProperty(key, version, description);
×
954
                                        as.saveGlobalProperty(gp);
×
955
                                } else if (!gp.getPropertyValue().equals(version)) {
×
956
                                        log.info("Updating global property " + key + " to version: " + version);
×
957
                                        gp.setDescription(description);
×
958
                                        gp.setPropertyValue(version);
×
959
                                        as.saveGlobalProperty(gp);
×
960
                                } else {
961
                                        log.error("Should not be here. GP property value and sqldiff version should not be equal");
×
962
                                }
963
                                
964
                        }
965
                        finally {
966
                                Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);
×
967
                        }
968
                        
969
                }
970
                
971
        }
×
972
        
973
        /**
974
         * Execute all not run changeSets in liquibase.xml for the given module
975
         * 
976
         * @param module the module being executed on
977
         */
978
        private static void runLiquibase(Module module) {
979
                ModuleClassLoader moduleClassLoader = getModuleClassLoader(module);
1✔
980
                boolean liquibaseFileExists = false;
1✔
981
                
982
                if (moduleClassLoader != null) {
1✔
983
                        try (InputStream inStream = moduleClassLoader.getResourceAsStream(MODULE_CHANGELOG_FILENAME)) {
1✔
984
                                liquibaseFileExists = (inStream != null);
1✔
985
                        }
986
                        catch (IOException ignored) {
×
987
                                
988
                        }
1✔
989
                }
990
                
991
                if (liquibaseFileExists) {
1✔
992
                        try {
993
                                // run liquibase.xml by Liquibase API
994
                                DatabaseUpdater.executeChangelog(MODULE_CHANGELOG_FILENAME, new Contexts(), null, moduleClassLoader);
×
995
                        }
996
                        catch (InputRequiredException e) {
×
997
                                // the user would be stepped through the questions returned here.
998
                                throw new ModuleException("Input during database updates is not yet implemented.", module.getName(), e);
×
999
                        }
1000
                        catch (Exception e) {
×
1001
                                throw new ModuleException("Unable to update data model using " + MODULE_CHANGELOG_FILENAME + ".",
×
1002
                                        module.getName(), e);
×
1003
                        }
×
1004
                }
1005
        }
1✔
1006
        
1007
        /**
1008
         * Runs through the advice and extension points and removes from api. <br>
1009
         * Also calls mod.Activator.shutdown()
1010
         * 
1011
         * @param mod module to stop
1012
         * @see ModuleFactory#stopModule(Module, boolean, boolean)
1013
         */
1014
        public static void stopModule(Module mod) {
1015
                stopModule(mod, false, false);
1✔
1016
        }
1✔
1017
        
1018
        /**
1019
         * Runs through the advice and extension points and removes from api.<br>
1020
         * Also calls mod.Activator.shutdown()
1021
         * 
1022
         * @param mod the module to stop
1023
         * @param isShuttingDown true if this is called during the process of shutting down openmrs
1024
         * @see #stopModule(Module, boolean, boolean)
1025
         */
1026
        public static void stopModule(Module mod, boolean isShuttingDown) {
1027
                stopModule(mod, isShuttingDown, false);
1✔
1028
        }
1✔
1029
        
1030
        /**
1031
         * Runs through the advice and extension points and removes from api.<br>
1032
         * <code>skipOverStartedProperty</code> should only be true when openmrs is stopping modules because
1033
         * it is shutting down. When normally stopping a module, use {@link #stopModule(Module)} (or leave
1034
         * value as false). This property controls whether the globalproperty is set for startup/shutdown.
1035
         * <br>
1036
         * Also calls module's {@link Activator#shutdown()}
1037
         * 
1038
         * @param mod module to stop
1039
         * @param skipOverStartedProperty true if we don't want to set &lt;moduleid&gt;.started to false
1040
         * @param isFailedStartup true if this is being called as a cleanup because of a failed module
1041
         *            startup
1042
         * @return list of dependent modules that were stopped because this module was stopped. This will
1043
         *         never be null.
1044
         */
1045
        public static List<Module> stopModule(Module mod, boolean skipOverStartedProperty, boolean isFailedStartup)
1046
                throws ModuleMustStartException {
1047
                
1048
                List<Module> dependentModulesStopped = new ArrayList<>();
1✔
1049
                
1050
                if (mod != null) {
1✔
1051
                        
1052
                        if (!ModuleFactory.isModuleStarted(mod)) {
1✔
1053
                                return dependentModulesStopped;
×
1054
                        }
1055
                        
1056
                        try {
1057
                                // if extends BaseModuleActivator
1058
                                if (mod.getModuleActivator() != null) {
1✔
1059
                                        mod.getModuleActivator().willStop();
1✔
1060
                                }
1061
                        }
1062
                        catch (Exception t) {
×
1063
                                log.warn("Unable to call module's Activator.willStop() method", t);
×
1064
                        }
1✔
1065
                        
1066
                        String moduleId = mod.getModuleId();
1✔
1067
                        
1068
                        // don't allow mandatory modules to be stopped
1069
                        // don't use database checks here because spring might be in a bad state
1070
                        if (!isFailedStartup && mod.isMandatory()) {
1✔
1071
                                throw new MandatoryModuleException(moduleId);
×
1072
                        }
1073
                        
1074
                        if (!isFailedStartup && ModuleConstants.CORE_MODULES.containsKey(moduleId)) {
1✔
1075
                                throw new OpenmrsCoreModuleException(moduleId);
×
1076
                        }
1077
                        
1078
                        String modulePackage = mod.getPackageName();
1✔
1079
                        
1080
                        // stop all dependent modules
1081
                        // copy modules to new list to avoid "concurrent modification exception"
1082
                        List<Module> startedModulesCopy = new ArrayList<>(getStartedModules());
1✔
1083
                        for (Module dependentModule : startedModulesCopy) {
1✔
1084
                                if (dependentModule != null && !dependentModule.equals(mod)
1✔
1085
                                        && isModuleRequiredByAnother(dependentModule, modulePackage)) {
1✔
1086
                                        dependentModulesStopped.add(dependentModule);
1✔
1087
                                        dependentModulesStopped.addAll(stopModule(dependentModule, skipOverStartedProperty, isFailedStartup));
1✔
1088
                                }
1089
                        }
1✔
1090
                        
1091
                        getStartedModulesMap().remove(moduleId);
1✔
1092
                        if (actualStartupOrder != null) {
1✔
1093
                                actualStartupOrder.remove(moduleId);
1✔
1094
                                for (Module depModule : dependentModulesStopped) {
1✔
1095
                                        actualStartupOrder.remove(depModule.getModuleId());
1✔
1096
                                }
1✔
1097
                        }
1098
                        
1099
                        if (!skipOverStartedProperty && !Context.isRefreshingContext()) {
1✔
1100
                                saveGlobalProperty(moduleId + ".started", "false", getGlobalPropertyStartedDescription(moduleId));
1✔
1101
                        }
1102
                        
1103
                        ModuleClassLoader moduleClassLoader = getModuleClassLoaderMap().get(mod);
1✔
1104
                        if (moduleClassLoader != null) {
1✔
1105
                                unregisterProvidedPackages(moduleClassLoader);
1✔
1106
                                
1107
                                log.debug("Mod was in classloader map.  Removing advice and extensions.");
1✔
1108
                                // remove all advice by this module
1109
                                try {
1110
                                        for (AdvicePoint advice : mod.getAdvicePoints()) {
1✔
1111
                                                Class cls;
1112
                                                try {
1113
                                                        cls = Context.loadClass(advice.getPoint());
×
1114
                                                        Object aopObject = advice.getClassInstance();
×
1115
                                                        if (Advisor.class.isInstance(aopObject)) {
×
1116
                                                                log.debug("adding advisor: " + aopObject.getClass());
×
1117
                                                                Context.removeAdvisor(cls, (Advisor) aopObject);
×
1118
                                                        } else {
1119
                                                                log.debug("Adding advice: " + aopObject.getClass());
×
1120
                                                                Context.removeAdvice(cls, (Advice) aopObject);
×
1121
                                                        }
1122
                                                }
1123
                                                catch (Exception t) {
×
1124
                                                        log.warn("Could not remove advice point: " + advice.getPoint(), t);
×
1125
                                                }
×
1126
                                        }
×
1127
                                }
1128
                                catch (Exception t) {
×
1129
                                        log.warn("Error while getting advicePoints from module: " + moduleId, t);
×
1130
                                }
1✔
1131
                                
1132
                                // remove all extensions by this module
1133
                                try {
1134
                                        for (Extension ext : mod.getExtensions()) {
1✔
1135
                                                String extId = ext.getExtensionId();
×
1136
                                                try {
1137
                                                        List<Extension> tmpExtensions = getExtensions(extId);
×
1138
                                                        tmpExtensions.remove(ext);
×
1139
                                                        getExtensionMap().put(extId, tmpExtensions);
×
1140
                                                }
1141
                                                catch (Exception exterror) {
×
1142
                                                        log.warn("Error while getting extension: " + ext, exterror);
×
1143
                                                }
×
1144
                                        }
×
1145
                                }
1146
                                catch (Exception t) {
×
1147
                                        log.warn("Error while getting extensions from module: " + moduleId, t);
×
1148
                                }
1✔
1149
                        }
1150
                        
1151
                        //Run the onShutdown() method for openmrs services in this module.
1152
                        List<OpenmrsService> services = Context.getModuleOpenmrsServices(modulePackage);
1✔
1153
                        if (services != null) {
1✔
1154
                                for (OpenmrsService service : services) {
1✔
1155
                                        service.onShutdown();
1✔
1156
                                }
1✔
1157
                        }
1158
                        
1159
                        try {
1160
                                if (mod.getModuleActivator() != null) {// extends BaseModuleActivator
1✔
1161
                                        mod.getModuleActivator().stopped();
1✔
1162
                                }
1163
                        }
1164
                        catch (Exception t) {
×
1165
                                log.warn("Unable to call module's Activator.shutdown() method", t);
×
1166
                        }
1✔
1167
                        
1168
                        //Since extensions are loaded by the module class loader which is about to be disposed,
1169
                        //we need to clear them, else we shall never be able to unload the class loader until
1170
                        //when we unload the module, hence resulting into two problems:
1171
                        // 1) Memory leakage for start/stop module.
1172
                        // 2) Calls to Context.getService(Service.class) which are made within these extensions 
1173
                        //          will throw APIException("Service not found: ") because their calls to Service.class
1174
                        //    will pass in a Class from the old module class loader (which loaded them) yet the
1175
                        //    ServiceContext will have new services from a new module class loader.
1176
                        //
1177
                        //Same thing applies to activator, moduleActivator and AdvicePoint classInstance.
1178
                        mod.getExtensions().clear();
1✔
1179
                        mod.setModuleActivator(null);
1✔
1180
                        mod.disposeAdvicePointsClassInstance();
1✔
1181
                        
1182
                        ModuleClassLoader cl = removeClassLoader(mod);
1✔
1183
                        if (cl != null) {
1✔
1184
                                cl.dispose();
1✔
1185
                                // remove files from lib cache
1186
                                File folder = OpenmrsClassLoader.getLibCacheFolder();
1✔
1187
                                File tmpModuleDir = new File(folder, moduleId);
1✔
1188
                                try {
1189
                                        OpenmrsUtil.deleteDirectory(tmpModuleDir);
1✔
1190
                                }
1191
                                catch (IOException e) {
×
1192
                                        log.warn("Unable to delete libcachefolder for " + moduleId);
×
1193
                                }
1✔
1194
                        }
1195
                }
1196
                
1197
                return dependentModulesStopped;
1✔
1198
        }
1199
        
1200
        /**
1201
         * Checks if a module is required by another
1202
         *
1203
         * @param dependentModule the module whose required modules are to be checked
1204
         * @param modulePackage the package of the module to check if required by another
1205
         * @return true if the module is required, else false
1206
         */
1207
        private static boolean isModuleRequiredByAnother(Module dependentModule, String modulePackage) {
1208
                return dependentModule.getRequiredModules() != null && dependentModule.getRequiredModules().contains(modulePackage);
1✔
1209
        }
1210
        
1211
        private static ModuleClassLoader removeClassLoader(Module mod) {
1212
                // create map if it is null
1213
                getModuleClassLoaderMap();
1✔
1214
                if (!moduleClassLoaders.containsKey(mod)) {
1✔
1215
                        log.warn("Module: " + mod.getModuleId() + " does not exist");
×
1216
                }
1217
                
1218
                return moduleClassLoaders.remove(mod);
1✔
1219
        }
1220
        
1221
        /**
1222
         * Removes module from module repository
1223
         * 
1224
         * @param mod module to unload
1225
         */
1226
        public static void unloadModule(Module mod) {
1227
                
1228
                // remove this module's advice and extensions
1229
                if (isModuleStarted(mod)) {
1✔
1230
                        stopModule(mod, true);
1✔
1231
                }
1232
                
1233
                // remove from list of loaded modules
1234
                getLoadedModules().remove(mod);
1✔
1235
                
1236
                if (mod != null) {
1✔
1237
                        // remove the file from the module repository
1238
                        File file = mod.getFile();
1✔
1239
                        
1240
                        boolean deleted = file.delete();
1✔
1241
                        if (!deleted) {
1✔
1242
                                file.deleteOnExit();
×
1243
                                log.warn("Could not delete " + file.getAbsolutePath());
×
1244
                        }
1245
                        
1246
                }
1247
        }
1✔
1248
        
1249
        /**
1250
         * Return all of the extensions associated with the given <code>pointId</code> Returns empty
1251
         * extension list if no modules extend this pointId
1252
         * 
1253
         * @param pointId
1254
         * @return List of extensions
1255
         */
1256
        public static List<Extension> getExtensions(String pointId) {
1257
                List<Extension> extensions;
1258
                Map<String, List<Extension>> extensionMap = getExtensionMap();
×
1259
                
1260
                // get all extensions for this exact pointId
1261
                extensions = extensionMap.get(pointId);
×
1262
                if (extensions == null) {
×
1263
                        extensions = new ArrayList<>();
×
1264
                }
1265
                
1266
                // if this pointId doesn't contain the separator character, search
1267
                // for this point prepended with each MEDIA TYPE
1268
                if (!pointId.contains(Extension.EXTENSION_ID_SEPARATOR)) {
×
1269
                        for (MEDIA_TYPE mediaType : Extension.MEDIA_TYPE.values()) {
×
1270
                                
1271
                                // get all extensions for this type and point id
1272
                                List<Extension> tmpExtensions = extensionMap.get(Extension.toExtensionId(pointId, mediaType));
×
1273
                                
1274
                                // 'extensions' should be a unique list
1275
                                if (tmpExtensions != null) {
×
1276
                                        for (Extension ext : tmpExtensions) {
×
1277
                                                if (!extensions.contains(ext)) {
×
1278
                                                        extensions.add(ext);
×
1279
                                                }
1280
                                        }
×
1281
                                }
1282
                        }
1283
                }
1284
                
1285
                log.debug("Getting extensions defined by : " + pointId);
×
1286
                return extensions;
×
1287
        }
1288
        
1289
        /**
1290
         * Return all of the extensions associated with the given <code>pointId</code> Returns
1291
         * getExtension(pointId) if no modules extend this pointId for given media type
1292
         * 
1293
         * @param pointId
1294
         * @param type Extension.MEDIA_TYPE
1295
         * @return List of extensions
1296
         */
1297
        public static List<Extension> getExtensions(String pointId, Extension.MEDIA_TYPE type) {
1298
                String key = Extension.toExtensionId(pointId, type);
×
1299
                List<Extension> extensions = getExtensionMap().get(key);
×
1300
                if (extensions != null) {
×
1301
                        log.debug("Getting extensions defined by : " + key);
×
1302
                        return extensions;
×
1303
                } else {
1304
                        return getExtensions(pointId);
×
1305
                }
1306
        }
1307
        
1308
        /**
1309
         * Get a list of required Privileges defined by the modules
1310
         * 
1311
         * @return <code>List&lt;Privilege&gt;</code> of the required privileges
1312
         */
1313
        public static List<Privilege> getPrivileges() {
1314
                
1315
                List<Privilege> privileges = new ArrayList<>();
1✔
1316
                
1317
                for (Module mod : getStartedModules()) {
1✔
1318
                        privileges.addAll(mod.getPrivileges());
×
1319
                }
×
1320
                
1321
                log.debug(privileges.size() + " new privileges");
1✔
1322
                
1323
                return privileges;
1✔
1324
        }
1325
        
1326
        /**
1327
         * Get a list of required GlobalProperties defined by the modules
1328
         * 
1329
         * @return <code>List&lt;GlobalProperty&gt;</code> object of the module's global properties
1330
         */
1331
        public static List<GlobalProperty> getGlobalProperties() {
1332
                
1333
                List<GlobalProperty> globalProperties = new ArrayList<>();
×
1334
                
1335
                for (Module mod : getStartedModules()) {
×
1336
                        globalProperties.addAll(mod.getGlobalProperties());
×
1337
                }
×
1338
                
1339
                log.debug(globalProperties.size() + " new global properties");
×
1340
                
1341
                return globalProperties;
×
1342
        }
1343
        
1344
        /**
1345
         * Checks whether the given module is activated
1346
         * 
1347
         * @param mod Module to check
1348
         * @return true if the module is started, false otherwise
1349
         */
1350
        public static boolean isModuleStarted(Module mod) {
1351
                return getStartedModulesMap().containsValue(mod);
1✔
1352
        }
1353
        
1354
        /**
1355
         * Checks whether the given module, identified by its id, is started.
1356
         * 
1357
         * @param moduleId module id. e.g formentry, logic
1358
         * @since 1.9
1359
         * @return true if the module is started, false otherwise
1360
         */
1361
        public static boolean isModuleStarted(String moduleId) {
1362
                return getStartedModulesMap().containsKey(moduleId);
1✔
1363
        }
1364
        
1365
        /**
1366
         * Get a module's classloader
1367
         * 
1368
         * @param mod Module to fetch the class loader for
1369
         * @return ModuleClassLoader pertaining to this module. Returns null if the module is not started
1370
         * @throws ModuleException if the module does not have a registered classloader
1371
         */
1372
        public static ModuleClassLoader getModuleClassLoader(Module mod) throws ModuleException {
1373
                ModuleClassLoader mcl = getModuleClassLoaderMap().get(mod);
1✔
1374
                
1375
                if (mcl == null) {
1✔
1376
                        log.debug("Module classloader not found for module with id: " + mod.getModuleId());
1✔
1377
                }
1378
                
1379
                return mcl;
1✔
1380
        }
1381
        
1382
        /**
1383
         * Get a module's classloader via the module id
1384
         * 
1385
         * @param moduleId <code>String</code> id of the module
1386
         * @return ModuleClassLoader pertaining to this module. Returns null if the module is not started
1387
         * @throws ModuleException if this module isn't started or doesn't have a classloader
1388
         * @see #getModuleClassLoader(Module)
1389
         */
1390
        public static ModuleClassLoader getModuleClassLoader(String moduleId) throws ModuleException {
1391
                Module mod = getStartedModulesMap().get(moduleId);
×
1392
                if (mod == null) {
×
1393
                        log.debug("Module id not found in list of started modules: " + moduleId);
×
1394
                }
1395
                
1396
                return getModuleClassLoader(mod);
×
1397
        }
1398
        
1399
        /**
1400
         * Returns all module classloaders This method will not return null
1401
         * 
1402
         * @return Collection&lt;ModuleClassLoader&gt; all known module classloaders or empty list.
1403
         */
1404
        public static Collection<ModuleClassLoader> getModuleClassLoaders() {
1405
                Map<Module, ModuleClassLoader> classLoaders = getModuleClassLoaderMap();
1✔
1406
                if (classLoaders.size() > 0) {
1✔
1407
                        return classLoaders.values();
1✔
1408
                }
1409
                
1410
                return Collections.emptyList();
1✔
1411
        }
1412
        
1413
        /**
1414
         * Return all current classloaders keyed on module object
1415
         * 
1416
         * @return Map&lt;Module, ModuleClassLoader&gt;
1417
         */
1418
        public static Map<Module, ModuleClassLoader> getModuleClassLoaderMap() {
1419
                if (moduleClassLoaders == null) {
1✔
1420
                        moduleClassLoaders = new WeakHashMap<>();
1✔
1421
                }
1422
                
1423
                return moduleClassLoaders;
1✔
1424
        }
1425
        
1426
        /**
1427
         * Return the current extension map keyed on extension point id
1428
         * 
1429
         * @return Map&lt;String, List&lt;Extension&gt;&gt;
1430
         */
1431
        public static Map<String, List<Extension>> getExtensionMap() {
1432
                if (extensionMap == null) {
×
1433
                        extensionMap = new WeakHashMap<>();
×
1434
                }
1435
                
1436
                return extensionMap;
×
1437
        }
1438
        
1439
        /**
1440
         * Tests whether all modules mentioned in module.requiredModules are loaded and started already (by
1441
         * being in the startedModules list)
1442
         * 
1443
         * @param module
1444
         * @return true/false boolean whether this module's required modules are all started
1445
         */
1446
        private static boolean requiredModulesStarted(Module module) {
1447
                //required
1448
                for (String reqModPackage : module.getRequiredModules()) {
1✔
1449
                        boolean started = false;
1✔
1450
                        for (Module mod : getStartedModules()) {
1✔
1451
                                if (mod.getPackageName().equals(reqModPackage)) {
1✔
1452
                                        String reqVersion = module.getRequiredModuleVersion(reqModPackage);
1✔
1453
                                        if (reqVersion == null || ModuleUtil.compareVersion(mod.getVersion(), reqVersion) >= 0) {
1✔
1454
                                                started = true;
1✔
1455
                                        }
1456
                                        break;
1457
                                }
1458
                        }
1✔
1459
                        
1460
                        if (!started) {
1✔
1461
                                return false;
1✔
1462
                        }
1463
                }
1✔
1464
                
1465
                return true;
1✔
1466
        }
1467
        
1468
        /**
1469
         * Update the module: 1) Download the new module 2) Unload the old module 3) Load/start the new
1470
         * module
1471
         * 
1472
         * @param mod
1473
         */
1474
        public static Module updateModule(Module mod) throws ModuleException {
1475
                if (mod.getDownloadURL() == null) {
×
1476
                        return mod;
×
1477
                }
1478
                
1479
                URL url;
1480
                try {
1481
                        url = new URL(mod.getDownloadURL());
×
1482
                }
1483
                catch (MalformedURLException e) {
×
1484
                        throw new ModuleException("Unable to download module update", e);
×
1485
                }
×
1486
                
1487
                unloadModule(mod);
×
1488
                
1489
                // copy content to a temporary file
1490
                InputStream inputStream = ModuleUtil.getURLStream(url);
×
1491
                log.warn("url pathname: " + url.getPath());
×
1492
                String filename = url.getPath().substring(url.getPath().lastIndexOf("/"));
×
1493
                File moduleFile = ModuleUtil.insertModuleFile(inputStream, filename);
×
1494
                
1495
                try {
1496
                        // load, and start the new module
1497
                        Module newModule = loadModule(moduleFile);
×
1498
                        startModule(newModule);
×
1499
                        return newModule;
×
1500
                }
1501
                catch (Exception e) {
×
1502
                        log.warn("Error while unloading old module and loading in new module");
×
1503
                        moduleFile.delete();
×
1504
                        return mod;
×
1505
                }
1506
                
1507
        }
1508
        
1509
        /**
1510
         * Validates the given token.
1511
         * <p>
1512
         * It is thread safe.
1513
         * 
1514
         * @param token
1515
         * @since 1.9.2
1516
         */
1517
        public static boolean isTokenValid(DaemonToken token) {
1518
                if (token == null) {
×
1519
                        return false;
×
1520
                } else {
1521
                        //We need to synchronize to guarantee that the last passed token is valid.
1522
                        synchronized (daemonTokens) {
×
1523
                                DaemonToken validToken = daemonTokens.get(token.getId());
×
1524
                                //Compare by reference to defend from overridden equals.
1525
                                return validToken == token;
×
1526
                        }
1527
                }
1528
        }
1529
        
1530
        /**
1531
         * Passes a daemon token to the given module.
1532
         * <p>
1533
         * The token is passed to that module's {@link ModuleActivator} if it implements
1534
         * {@link DaemonTokenAware}.
1535
         * <p>
1536
         * This method is called automatically before {@link ModuleActivator#contextRefreshed()} or
1537
         * {@link ModuleActivator#started()}. Note that it may be called multiple times and there is no
1538
         * guarantee that it will always pass the same token. The last passed token is valid, whereas
1539
         * previously passed tokens may be invalidated.
1540
         * <p>
1541
         * It is thread safe.
1542
         * 
1543
         * @param module
1544
         * @since 1.9.2
1545
         */
1546
        static void passDaemonToken(Module module) {
1547
                ModuleActivator moduleActivator = module.getModuleActivator();
×
1548
                if (moduleActivator instanceof DaemonTokenAware) {
×
1549
                        DaemonToken daemonToken = getDaemonToken(module);
×
1550
                        ((DaemonTokenAware) module.getModuleActivator()).setDaemonToken(daemonToken);
×
1551
                }
1552
        }
×
1553
        
1554
        /**
1555
         * Gets a new or existing token. Uses weak references for tokens so that they are garbage collected
1556
         * when not needed.
1557
         * <p>
1558
         * It is thread safe.
1559
         * 
1560
         * @param module
1561
         * @return the token
1562
         */
1563
        private static DaemonToken getDaemonToken(Module module) {
1564
                synchronized (daemonTokens) {
×
1565
                        DaemonToken token = daemonTokens.get(module.getModuleId());
×
1566
                        if (token != null) {
×
1567
                                return token;
×
1568
                        }
1569
                        
1570
                        token = new DaemonToken(module.getModuleId());
×
1571
                        daemonTokens.put(module.getModuleId(), token);
×
1572
                        
1573
                        return token;
×
1574
                }
1575
        }
1576
        
1577
        /**
1578
         * Returns the description for the [moduleId].started global property
1579
         * 
1580
         * @param moduleId
1581
         * @return description to use for the .started property
1582
         */
1583
        private static String getGlobalPropertyStartedDescription(String moduleId) {
1584
                String ret = "DO NOT MODIFY. true/false whether or not the " + moduleId;
1✔
1585
                ret += " module has been started.  This is used to make sure modules that were running ";
1✔
1586
                ret += " prior to a restart are started again";
1✔
1587
                
1588
                return ret;
1✔
1589
        }
1590
        
1591
        /**
1592
         * Returns the description for the [moduleId].mandatory global property
1593
         * 
1594
         * @param moduleId
1595
         * @return description to use for .mandatory property
1596
         */
1597
        private static String getGlobalPropertyMandatoryModuleDescription(String moduleId) {
1598
                String ret = "true/false whether or not the " + moduleId;
1✔
1599
                ret += " module MUST start when openmrs starts.  This is used to make sure that mission critical";
1✔
1600
                ret += " modules are always running if openmrs is running.";
1✔
1601
                
1602
                return ret;
1✔
1603
        }
1604
        
1605
        /**
1606
         * Convenience method to save a global property with the given value. Proxy privileges are added so
1607
         * that this can occur at startup.
1608
         * 
1609
         * @param key the property for this global property
1610
         * @param value the value for this global property
1611
         * @param desc the description
1612
         * @see AdministrationService#saveGlobalProperty(GlobalProperty)
1613
         */
1614
        private static void saveGlobalProperty(String key, String value, String desc) {
1615
                try {
1616
                        AdministrationService as = Context.getAdministrationService();
1✔
1617
                        GlobalProperty gp = as.getGlobalPropertyObject(key);
1✔
1618
                        if (gp == null) {
1✔
1619
                                gp = new GlobalProperty(key, value, desc);
1✔
1620
                        } else {
1621
                                gp.setPropertyValue(value);
1✔
1622
                        }
1623
                        
1624
                        as.saveGlobalProperty(gp);
1✔
1625
                }
1626
                catch (Exception e) {
×
1627
                        log.warn("Unable to save the global property", e);
×
1628
                }
1✔
1629
        }
1✔
1630
        
1631
        /**
1632
         * Convenience method used to identify module interdependencies and alert the user before modules
1633
         * are shut down.
1634
         * 
1635
         * @param moduleId the moduleId used to identify the module being validated
1636
         * @return List&lt;dependentModules&gt; the list of moduleId's which depend on the module about to
1637
         *         be shutdown.
1638
         * @since 1.10
1639
         */
1640
        public static List<String> getDependencies(String moduleId) {
1641
                List<String> dependentModules = null;
×
1642
                Module module = getModuleById(moduleId);
×
1643
                
1644
                Map<String, Module> startedModules = getStartedModulesMap();
×
1645
                String modulePackage = module.getPackageName();
×
1646
                
1647
                for (Entry<String, Module> entry : startedModules.entrySet()) {
×
1648
                        if (!moduleId.equals(entry.getKey()) && entry.getValue().getRequiredModules().contains(modulePackage)) {
×
1649
                                if (dependentModules == null) {
×
1650
                                        dependentModules = new ArrayList<>();
×
1651
                                }
1652
                                dependentModules.add(entry.getKey() + " " + entry.getValue().getVersion());
×
1653
                        }
1654
                }
×
1655
                return dependentModules;
×
1656
        }
1657
}
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