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

openmrs / openmrs-core / 10290281517

07 Aug 2024 07:08PM CUT coverage: 64.834% (-0.1%) from 64.952%
10290281517

push

github

web-flow
maven(deps): bump org.apache.commons:commons-lang3 from 3.15.0 to 3.16.0 (#4707)

Bumps org.apache.commons:commons-lang3 from 3.15.0 to 3.16.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

22918 of 35349 relevant lines covered (64.83%)

0.65 hits per line

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

51.19
/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.util.ArrayList;
18
import java.util.Arrays;
19
import java.util.Collection;
20
import java.util.Collections;
21
import java.util.Comparator;
22
import java.util.HashMap;
23
import java.util.HashSet;
24
import java.util.LinkedHashSet;
25
import java.util.List;
26
import java.util.Map;
27
import java.util.Map.Entry;
28
import java.util.Set;
29
import java.util.SortedMap;
30
import java.util.concurrent.ConcurrentHashMap;
31
import java.util.concurrent.ExecutionException;
32

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

58
import liquibase.Contexts;
59

60
/**
61
 * Methods for loading, starting, stopping, and storing OpenMRS modules
62
 */
63
public class ModuleFactory {
64
        
65
        private ModuleFactory() {
66
        }
67
        
68
        private static final Logger log = LoggerFactory.getLogger(ModuleFactory.class);
1✔
69
        
70
        protected static final Cache<String, Module> loadedModules = CacheBuilder.newBuilder()
1✔
71
                .softValues().build();
1✔
72
        
73
        protected static final Cache<String, Module> startedModules = CacheBuilder.newBuilder()
1✔
74
                .softValues().build();
1✔
75
        
76
        protected static final Map<String, List<Extension>> extensionMap = new HashMap<>();
1✔
77
        
78
        // maps to keep track of the memory and objects to free/close
79
        protected static final Cache<Module, ModuleClassLoader> moduleClassLoaders = CacheBuilder.newBuilder().weakKeys()
1✔
80
                .softValues().build();
1✔
81
        
82
        private static final Map<String, Set<ModuleClassLoader>> providedPackages = new ConcurrentHashMap<>();
1✔
83
        
84
        // the name of the file within a module file
85
        private static final String MODULE_CHANGELOG_FILENAME = "liquibase.xml";
86
        
87
        private static final Cache<String, DaemonToken> daemonTokens = CacheBuilder.newBuilder().softValues().build();
1✔
88
        
89
        private static final Set<String> actualStartupOrder = new LinkedHashSet<>();
1✔
90
        
91
        /**
92
         * Add a module (in the form of a jar file) to the list of openmrs modules Returns null if an error
93
         * occurred and/or module was not successfully loaded
94
         *
95
         * @param moduleFile
96
         * @return Module
97
         */
98
        public static Module loadModule(File moduleFile) throws ModuleException {
99
                
100
                return loadModule(moduleFile, true);
×
101
                
102
        }
103
        
104
        /**
105
         * Add a module (in the form of a jar file) to the list of openmrs modules Returns null if an error
106
         * occurred and/or module was not successfully loaded
107
         *
108
         * @param moduleFile
109
         * @param replaceIfExists unload a module that has the same moduleId if one is loaded already
110
         * @return Module
111
         */
112
        public static Module loadModule(File moduleFile, Boolean replaceIfExists) throws ModuleException {
113
                Module module = new ModuleFileParser(Context.getMessageSourceService()).parse(moduleFile);
1✔
114
                
115
                if (module != null) {
1✔
116
                        loadModule(module, replaceIfExists);
1✔
117
                }
118
                
119
                return module;
1✔
120
        }
121
        
122
        /**
123
         * Add a module to the list of openmrs modules
124
         *
125
         * @param module
126
         * @param replaceIfExists unload a module that has the same moduleId if one is loaded already
127
         *            <strong>Should</strong> load module if it is currently not loaded
128
         *            <strong>Should</strong> not load module if already loaded <strong>Should</strong>
129
         *            always load module if replacement is wanted <strong>Should</strong> not load an older
130
         *            version of the same module <strong>Should</strong> load a newer version of the same
131
         *            module
132
         * @return module the module that was loaded or if the module exists already with the same version,
133
         *         the old module
134
         */
135
        public static Module loadModule(Module module, Boolean replaceIfExists) throws ModuleException {
136
                
137
                log.debug("Adding module {} to the module queue", module.getName());
1✔
138
                
139
                Module oldModule = getLoadedModulesMap().get(module.getModuleId());
1✔
140
                if (oldModule != null) {
1✔
141
                        int versionComparison = ModuleUtil.compareVersion(oldModule.getVersion(), module.getVersion());
1✔
142
                        if (versionComparison < 0) {
1✔
143
                                // if oldModule version is lower, unload it and use the new
144
                                unloadModule(oldModule);
1✔
145
                        } else if (versionComparison == 0) {
1✔
146
                                if (replaceIfExists) {
1✔
147
                                        // if the versions are the same and we're told to replaceIfExists, use the new
148
                                        unloadModule(oldModule);
1✔
149
                                } else {
150
                                        // if the versions are equal and we're not told to replaceIfExists, jump out of here in a bad way
151
                                        throw new ModuleException("A module with the same id and version already exists", module.getModuleId());
1✔
152
                                }
153
                        } else {
154
                                // if the older (already loaded) module is newer, keep that original one that was loaded. return that one.
155
                                return oldModule;
1✔
156
                        }
157
                }
158
                
159
                getLoadedModulesMap().put(module.getModuleId(), module);
1✔
160
                
161
                return module;
1✔
162
        }
163
        
164
        /**
165
         * Load OpenMRS modules from <code>OpenmrsUtil.getModuleRepository()</code>
166
         */
167
        public static void loadModules() {
168
                
169
                // load modules from the user's module repository directory
170
                File modulesFolder = ModuleUtil.getModuleRepository();
×
171
                
172
                log.debug("Loading modules from: {}", modulesFolder.getAbsolutePath());
×
173
                
174
                File[] files = modulesFolder.listFiles();
×
175
                if (modulesFolder.isDirectory() && files != null) {
×
176
                        loadModules(Arrays.asList(files));
×
177
                } else {
178
                        log.error("modules folder: '" + modulesFolder.getAbsolutePath() + "' is not a directory or IO error occurred");
×
179
                }
180
        }
×
181
        
182
        /**
183
         * Attempt to load the given files as OpenMRS modules
184
         *
185
         * @param modulesToLoad the list of files to try and load <strong>Should</strong> not crash when
186
         *            file is not found or broken <strong>Should</strong> setup requirement mappings for
187
         *            every module <strong>Should</strong> not start the loaded modules
188
         */
189
        public static void loadModules(List<File> modulesToLoad) {
190
                // loop over the modules and load all the modules that we can
191
                for (File f : modulesToLoad) {
1✔
192
                        if (f.exists()) {
1✔
193
                                // ignore .svn folder and the like
194
                                if (!f.getName().startsWith(".")) {
1✔
195
                                        try {
196
                                                // last module loaded wins
197
                                                Module mod = loadModule(f, true);
1✔
198
                                                log.debug("Loaded module: " + mod + " successfully");
1✔
199
                                        }
200
                                        catch (Exception e) {
×
201
                                                log.error("Unable to load file in module directory: " + f + ". Skipping file.", e);
×
202
                                        }
1✔
203
                                }
204
                        } else {
205
                                log.error("Could not find file in module directory: " + f);
1✔
206
                        }
207
                }
1✔
208
                
209
                //inform modules, that they can't start before other modules
210
                
211
                Map<String, Module> loadedModulesMap = getLoadedModulesMapPackage();
1✔
212
                for (Module m : loadedModulesMap.values()) {
1✔
213
                        Map<String, String> startBeforeModules = m.getStartBeforeModulesMap();
1✔
214
                        if (startBeforeModules.size() > 0) {
1✔
215
                                for (String s : startBeforeModules.keySet()) {
1✔
216
                                        Module mod = loadedModulesMap.get(s);
1✔
217
                                        if (mod != null) {
1✔
218
                                                mod.addRequiredModule(m.getPackageName(), m.getVersion());
1✔
219
                                        }
220
                                }
1✔
221
                        }
222
                }
1✔
223
        }
1✔
224
        
225
        /**
226
         * Try to start all of the loaded modules that have the global property <i>moduleId</i>.started is
227
         * set to "true" or the property does not exist. Otherwise, leave it as only "loaded"<br>
228
         * <br>
229
         * Modules that are already started will be skipped.
230
         */
231
        public static void startModules() {
232
                
233
                // loop over and try starting each of the loaded modules
234
                if (!getLoadedModules().isEmpty()) {
1✔
235
                        
236
                        List<Module> modules = getModulesThatShouldStart();
1✔
237
                        
238
                        try {
239
                                modules = getModulesInStartupOrder(modules);
1✔
240
                        }
241
                        catch (CycleException ex) {
×
242
                                String message = getCyclicDependenciesMessage(ex.getMessage());
×
243
                                log.error(message, ex);
×
244
                                notifySuperUsersAboutCyclicDependencies(ex);
×
245
                                modules = (List<Module>) ex.getExtraData();
×
246
                        }
1✔
247
                        
248
                        // try and start the modules that should be started
249
                        for (Module mod : modules) {
1✔
250
                                
251
                                if (mod.isStarted()) {
1✔
252
                                        // skip over modules that are already started
253
                                        continue;
×
254
                                }
255
                                
256
                                // Skip module if required ones are not started
257
                                if (!requiredModulesStarted(mod)) {
1✔
258
                                        String message = getFailedToStartModuleMessage(mod);
×
259
                                        log.error(message);
×
260
                                        mod.setStartupErrorMessage(message);
×
261
                                        notifySuperUsersAboutModuleFailure(mod);
×
262
                                        continue;
×
263
                                }
264
                                
265
                                try {
266
                                        log.debug("starting module: {}", mod.getModuleId());
1✔
267
                                        startModule(mod);
1✔
268
                                }
269
                                catch (Exception e) {
×
270
                                        log.error("Error while starting module: " + mod.getName(), e);
×
271
                                        mod.setStartupErrorMessage("Error while starting module", e);
×
272
                                        notifySuperUsersAboutModuleFailure(mod);
×
273
                                }
1✔
274
                        }
1✔
275
                }
276
        }
1✔
277
        
278
        /**
279
         * Obtain the list of modules that should be started
280
         *
281
         * @return list of modules
282
         */
283
        private static List<Module> getModulesThatShouldStart() {
284
                List<Module> modules = new ArrayList<>();
1✔
285
                
286
                AdministrationService adminService = Context.getAdministrationService();
1✔
287
                
288
                for (Module mod : getLoadedModulesCoreFirst()) {
1✔
289
                        
290
                        String key = mod.getModuleId() + ".started";
1✔
291
                        String startedProp = adminService.getGlobalProperty(key, null);
1✔
292
                        String mandatoryProp = adminService.getGlobalProperty(mod.getModuleId() + ".mandatory", null);
1✔
293
                        
294
                        boolean isCoreToOpenmrs = mod.isCore() && !ModuleUtil.ignoreCoreModules();
1✔
295
                        
296
                        // if a 'moduleid.started' property doesn't exist, start the module anyway
297
                        // as this is probably the first time they are loading it
298
                        if (startedProp == null || "true".equals(startedProp) || "true".equalsIgnoreCase(mandatoryProp)
1✔
299
                                || mod.isMandatory() || isCoreToOpenmrs) {
×
300
                                modules.add(mod);
1✔
301
                        }
302
                }
1✔
303
                return modules;
1✔
304
        }
305
        
306
        /**
307
         * Sort modules in startup order based on required and aware-of dependencies
308
         *
309
         * @param modules list of modules to sort
310
         * @return list of modules sorted by dependencies
311
         * @throws CycleException
312
         */
313
        public static List<Module> getModulesInStartupOrder(Collection<Module> modules) throws CycleException {
314
                Graph<Module> graph = new Graph<>();
1✔
315
                
316
                for (Module mod : modules) {
1✔
317
                        
318
                        graph.addNode(mod);
1✔
319
                        
320
                        // Required dependencies
321
                        for (String key : mod.getRequiredModules()) {
1✔
322
                                Module module = getModuleByPackage(key);
1✔
323
                                Module fromNode = graph.getNode(module);
1✔
324
                                if (fromNode == null) {
1✔
325
                                        fromNode = module;
1✔
326
                                }
327
                                
328
                                if (fromNode != null) {
1✔
329
                                        graph.addEdge(graph.new Edge(
1✔
330
                                                fromNode,
331
                                                mod));
332
                                }
333
                        }
1✔
334
                        
335
                        // Aware-of dependencies
336
                        for (String key : mod.getAwareOfModules()) {
1✔
337
                                Module module = getModuleByPackage(key);
1✔
338
                                Module fromNode = graph.getNode(module);
1✔
339
                                if (fromNode == null) {
1✔
340
                                        fromNode = module;
1✔
341
                                }
342
                                
343
                                if (fromNode != null) {
1✔
344
                                        graph.addEdge(graph.new Edge(
×
345
                                                fromNode,
346
                                                mod));
347
                                }
348
                        }
1✔
349
                }
1✔
350
                
351
                return graph.topologicalSort();
1✔
352
        }
353
        
354
        /**
355
         * Send an Alert to all super users that the given module did not start successfully.
356
         *
357
         * @param mod The Module that failed
358
         */
359
        private static void notifySuperUsersAboutModuleFailure(Module mod) {
360
                try {
361
                        // Add the privileges necessary for notifySuperUsers
362
                        Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
363
                        
364
                        // Send an alert to all administrators
365
                        Context.getAlertService().notifySuperUsers("Module.startupError.notification.message", null, mod.getName());
×
366
                }
367
                catch (Exception e) {
×
368
                        log.error("Unable to send an alert to the super users", e);
×
369
                }
370
                finally {
371
                        // Remove added privileges
372
                        Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
373
                }
374
        }
×
375
        
376
        /**
377
         * Send an Alert to all super users that modules did not start due to cyclic dependencies
378
         */
379
        private static void notifySuperUsersAboutCyclicDependencies(Exception ex) {
380
                try {
381
                        Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
382
                        Context.getAlertService().notifySuperUsers("Module.error.cyclicDependencies", ex, ex.getMessage());
×
383
                }
384
                catch (Exception e) {
×
385
                        log.error("Unable to send an alert to the super users", e);
×
386
                }
387
                finally {
388
                        Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
389
                }
390
        }
×
391
        
392
        /**
393
         * Returns all modules found/loaded into the system (started and not started), with the core modules
394
         * at the start of that list
395
         *
396
         * @return <code>List&lt;Module&gt;</code> of the modules loaded into the system, with the core
397
         *         modules first.
398
         */
399
        public static List<Module> getLoadedModulesCoreFirst() {
400
                List<Module> list = new ArrayList<>(getLoadedModules());
1✔
401
                final Collection<String> coreModuleIds = ModuleConstants.CORE_MODULES.keySet();
1✔
402
                list.sort((left, right) -> {
1✔
403
                        Integer leftVal = coreModuleIds.contains(left.getModuleId()) ? 0 : 1;
1✔
404
                        Integer rightVal = coreModuleIds.contains(right.getModuleId()) ? 0 : 1;
1✔
405
                        return leftVal.compareTo(rightVal);
1✔
406
                });
407
                return list;
1✔
408
        }
409
        
410
        /**
411
         * Convenience method to return a List of Strings containing a description of which modules the
412
         * passed module requires but which are not started. The returned description of each module is the
413
         * moduleId followed by the required version if one is specified
414
         *
415
         * @param module the module to check required modules for
416
         * @return List&lt;String&gt; of module names + optional required versions: "org.openmrs.formentry
417
         *         1.8, org.rg.patientmatching"
418
         */
419
        private static List<String> getMissingRequiredModules(Module module) {
420
                List<String> ret = new ArrayList<>();
×
421
                for (String moduleName : module.getRequiredModules()) {
×
422
                        boolean started = false;
×
423
                        for (Module mod : getStartedModules()) {
×
424
                                if (mod.getPackageName().equals(moduleName)) {
×
425
                                        String reqVersion = module.getRequiredModuleVersion(moduleName);
×
426
                                        if (reqVersion == null || ModuleUtil.compareVersion(mod.getVersion(), reqVersion) >= 0) {
×
427
                                                started = true;
×
428
                                        }
429
                                        break;
430
                                }
431
                        }
×
432
                        
433
                        if (!started) {
×
434
                                String moduleVersion = module.getRequiredModuleVersion(moduleName);
×
435
                                moduleName = moduleName.replace("org.openmrs.module.", "").replace("org.openmrs.", "");
×
436
                                ret.add(moduleName + (moduleVersion != null ? " " + moduleVersion : ""));
×
437
                        }
438
                }
×
439
                return ret;
×
440
        }
441
        
442
        /**
443
         * Returns all modules found/loaded into the system (started and not started)
444
         *
445
         * @return <code>Collection&lt;Module&gt;</code> of the modules loaded into the system
446
         */
447
        public static Collection<Module> getLoadedModules() {
448
                if (getLoadedModulesMap().size() > 0) {
1✔
449
                        return getLoadedModulesMap().values();
1✔
450
                }
451
                
452
                return Collections.emptyList();
1✔
453
        }
454
        
455
        /**
456
         * Returns all modules found/loaded into the system (started and not started) in the form of a
457
         * map&lt;ModuleId, Module&gt;
458
         *
459
         * @return map&lt;ModuleId, Module&gt;
460
         */
461
        public static Map<String, Module> getLoadedModulesMap() {
462
                return loadedModules.asMap();
1✔
463
        }
464
        
465
        /**
466
         * Returns all modules found/loaded into the system (started and not started) in the form of a
467
         * map&lt;PackageName, Module&gt;
468
         *
469
         * @return map&lt;PackageName, Module&gt;
470
         */
471
        public static Map<String, Module> getLoadedModulesMapPackage() {
472
                Map<String, Module> map = new HashMap<>();
1✔
473
                for (Module loadedModule : getLoadedModulesMap().values()) {
1✔
474
                        map.put(loadedModule.getPackageName(), loadedModule);
1✔
475
                }
1✔
476
                return map;
1✔
477
        }
478
        
479
        /**
480
         * Returns the modules that have been successfully started
481
         *
482
         * @return <code>Collection&lt;Module&gt;</code> of the started modules
483
         */
484
        public static Collection<Module> getStartedModules() {
485
                if (getStartedModulesMap().size() > 0) {
1✔
486
                        return getStartedModulesMap().values();
1✔
487
                }
488
                
489
                return Collections.emptyList();
1✔
490
        }
491
        
492
        public static List<Module> getStartedModulesInOrder() {
493
                List<Module> modules = new ArrayList<>();
×
494
                if (actualStartupOrder != null) {
×
495
                        for (String moduleId : actualStartupOrder) {
×
496
                                modules.add(getStartedModulesMap().get(moduleId));
×
497
                        }
×
498
                } else {
499
                        modules.addAll(getStartedModules());
×
500
                }
501
                return modules;
×
502
        }
503
        
504
        /**
505
         * Returns the modules that have been successfully started in the form of a map&lt;ModuleId,
506
         * Module&gt;
507
         *
508
         * @return Map&lt;ModuleId, Module&gt;
509
         */
510
        public static Map<String, Module> getStartedModulesMap() {
511
                return startedModules.asMap();
1✔
512
        }
513
        
514
        /**
515
         * @param moduleId
516
         * @return Module matching module id or null if none
517
         */
518
        public static Module getModuleById(String moduleId) {
519
                return getLoadedModulesMap().get(moduleId);
1✔
520
        }
521
        
522
        /**
523
         * @param moduleId
524
         * @return Module matching moduleId, if it is started or null otherwise
525
         */
526
        public static Module getStartedModuleById(String moduleId) {
527
                return getStartedModulesMap().get(moduleId);
1✔
528
        }
529
        
530
        /**
531
         * @param modulePackage
532
         * @return Module matching module package or null if none
533
         */
534
        public static Module getModuleByPackage(String modulePackage) {
535
                for (Module mod : getLoadedModulesMap().values()) {
1✔
536
                        if (mod.getPackageName().equals(modulePackage)) {
1✔
537
                                return mod;
1✔
538
                        }
539
                }
1✔
540
                return null;
1✔
541
        }
542
        
543
        /**
544
         * @see #startModule(Module, boolean, AbstractRefreshableApplicationContext)
545
         * @see #startModuleInternal(Module)
546
         * @see Daemon#startModule(Module)
547
         */
548
        public static Module startModule(Module module) throws ModuleException {
549
                return startModule(module, false, null);
1✔
550
        }
551
        
552
        /**
553
         * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
554
         * Module's activator. This method is run in a new thread and is authenticated as the Daemon user.
555
         * If a non null application context is passed in, it gets refreshed to make the module's services
556
         * available
557
         *
558
         * @param module Module to start
559
         * @param isOpenmrsStartup Specifies whether this module is being started at application startup or
560
         *            not, this argument is ignored if a null application context is passed in
561
         * @param applicationContext the spring application context instance to refresh
562
         * @throws ModuleException if the module throws any kind of error at startup or in an activator
563
         * @see #startModuleInternal(Module, boolean, AbstractRefreshableApplicationContext)
564
         * @see Daemon#startModule(Module, boolean, AbstractRefreshableApplicationContext)
565
         */
566
        public static Module startModule(Module module, boolean isOpenmrsStartup,
567
                AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
568
                
569
                if (!requiredModulesStarted(module)) {
1✔
570
                        int missingModules = 0;
1✔
571
                        
572
                        for (String packageName : module.getRequiredModulesMap().keySet()) {
1✔
573
                                Module mod = getModuleByPackage(packageName);
1✔
574
                                
575
                                // mod not installed
576
                                if (mod == null) {
1✔
577
                                        missingModules++;
×
578
                                        continue;
×
579
                                }
580
                                
581
                                if (!mod.isStarted()) {
1✔
582
                                        startModule(mod);
1✔
583
                                }
584
                        }
1✔
585
                        
586
                        if (missingModules > 0) {
1✔
587
                                String message = getFailedToStartModuleMessage(module);
×
588
                                log.error(message);
×
589
                                module.setStartupErrorMessage(message);
×
590
                                notifySuperUsersAboutModuleFailure(module);
×
591
                                // instead of return null, i realized that Daemon.startModule() always returns a Module
592
                                // object,irrespective of whether the startup succeeded
593
                                return module;
×
594
                        }
595
                }
596
                return Daemon.startModule(module, isOpenmrsStartup, applicationContext);
1✔
597
        }
598
        
599
        /**
600
         * This method should not be called directly.<br>
601
         * <br>
602
         * The {@link #startModule(Module)} (and hence {@link Daemon#startModule(Module)}) calls this method
603
         * in a new Thread and is authenticated as the {@link Daemon} user<br>
604
         * <br>
605
         * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
606
         * Module's activator.
607
         *
608
         * @param module Module to start
609
         */
610
        public static Module startModuleInternal(Module module) throws ModuleException {
611
                return startModuleInternal(module, false, null);
×
612
        }
613
        
614
        /**
615
         * This method should not be called directly.<br>
616
         * <br>
617
         * The {@link #startModule(Module)} (and hence {@link Daemon#startModule(Module)}) calls this method
618
         * in a new Thread and is authenticated as the {@link Daemon} user<br>
619
         * <br>
620
         * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
621
         * Module's activator. <br>
622
         * <br>
623
         * If a non null application context is passed in, it gets refreshed to make the module's services
624
         * available
625
         *
626
         * @param module Module to start
627
         * @param isOpenmrsStartup Specifies whether this module is being started at application startup or
628
         *            not, this argument is ignored if a null application context is passed in
629
         * @param applicationContext the spring application context instance to refresh
630
         */
631
        public static Module startModuleInternal(Module module, boolean isOpenmrsStartup,
632
                AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
633
                
634
                if (module != null) {
1✔
635
                        String moduleId = module.getModuleId();
1✔
636
                        
637
                        try {
638
                                
639
                                // check to be sure this module can run with our current version
640
                                // of OpenMRS code
641
                                String requireVersion = module.getRequireOpenmrsVersion();
1✔
642
                                ModuleUtil.checkRequiredVersion(OpenmrsConstants.OPENMRS_VERSION_SHORT, requireVersion);
1✔
643
                                
644
                                // check for required modules
645
                                if (!requiredModulesStarted(module)) {
1✔
646
                                        throw new ModuleException(getFailedToStartModuleMessage(module));
×
647
                                }
648
                                
649
                                // fire up the classloader for this module
650
                                ModuleClassLoader moduleClassLoader = new ModuleClassLoader(module, ModuleFactory.class.getClassLoader());
1✔
651
                                getModuleClassLoaderMap().put(module, moduleClassLoader);
1✔
652
                                registerProvidedPackages(moduleClassLoader);
1✔
653
                                
654
                                // don't load the advice objects into the Context
655
                                // At startup, the spring context isn't refreshed until all modules
656
                                // have been loaded.  This causes errors if called here during a
657
                                // module's startup if one of these advice points is on another
658
                                // module because that other module's service won't have been loaded
659
                                // into spring yet.  All advice for all modules must be reloaded
660
                                // a spring context refresh anyway
661
                                
662
                                // map extension point to a list of extensions for this module only
663
                                Map<String, List<Extension>> moduleExtensionMap = new HashMap<>();
1✔
664
                                for (Extension ext : module.getExtensions()) {
1✔
665
                                        
666
                                        String extId = ext.getExtensionId();
×
667
                                        List<Extension> tmpExtensions = moduleExtensionMap.computeIfAbsent(extId, k -> new ArrayList<>());
×
668
                                        
669
                                        tmpExtensions.add(ext);
×
670
                                }
×
671
                                
672
                                // Sort this module's extensions, and merge them into the full extensions map
673
                                Comparator<Extension> sortOrder = (e1, e2) -> Integer.valueOf(e1.getOrder()).compareTo(e2.getOrder());
1✔
674
                                for (Map.Entry<String, List<Extension>> moduleExtensionEntry : moduleExtensionMap.entrySet()) {
1✔
675
                                        // Sort this module's extensions for current extension point
676
                                        List<Extension> sortedModuleExtensions = moduleExtensionEntry.getValue();
×
677
                                        sortedModuleExtensions.sort(sortOrder);
×
678
                                        
679
                                        // Get existing extensions, and append the ones from the new module
680
                                        List<Extension> extensions = getExtensionMap().computeIfAbsent(moduleExtensionEntry.getKey(),
×
681
                                                k -> new ArrayList<>());
×
682
                                        for (Extension ext : sortedModuleExtensions) {
×
683
                                                log.debug("Adding to mapping ext: " + ext.getExtensionId() + " ext.class: " + ext.getClass());
×
684
                                                extensions.add(ext);
×
685
                                        }
×
686
                                }
×
687
                                
688
                                // run the module's sql update script
689
                                // This and the property updates are the only things that can't
690
                                // be undone at startup, so put these calls after any other
691
                                // calls that might hinder startup
692
                                SortedMap<String, String> diffs = SqlDiffFileParser.getSqlDiffs(module);
1✔
693
                                
694
                                try {
695
                                        // this method must check and run queries against the database.
696
                                        // to do this, it must be "authenticated".  Give the current
697
                                        // "user" the proxy privilege so this can be done. ("user" might
698
                                        // be nobody because this is being run at startup)
699
                                        Context.addProxyPrivilege("");
1✔
700
                                        
701
                                        for (Map.Entry<String, String> entry : diffs.entrySet()) {
1✔
702
                                                String version = entry.getKey();
×
703
                                                String sql = entry.getValue();
×
704
                                                if (StringUtils.hasText(sql)) {
×
705
                                                        runDiff(module, version, sql);
×
706
                                                }
707
                                        }
×
708
                                }
709
                                finally {
710
                                        // take the "authenticated" privilege away from the current "user"
711
                                        Context.removeProxyPrivilege("");
1✔
712
                                }
713
                                
714
                                // run module's optional liquibase.xml immediately after sqldiff.xml
715
                                runLiquibase(module);
1✔
716
                                
717
                                // effectively mark this module as started successfully
718
                                getStartedModulesMap().put(moduleId, module);
1✔
719

720
                                actualStartupOrder.add(moduleId);
1✔
721
                                
722
                                try {
723
                                        // save the state of this module for future restarts
724
                                        saveGlobalProperty(moduleId + ".started", "true", getGlobalPropertyStartedDescription(moduleId));
1✔
725
                                        
726
                                        // save the mandatory status
727
                                        saveGlobalProperty(moduleId + ".mandatory", String.valueOf(module.isMandatory()),
1✔
728
                                                getGlobalPropertyMandatoryModuleDescription(moduleId));
1✔
729
                                }
730
                                catch (Exception e) {
×
731
                                        // pass over errors because this doesn't really concern startup
732
                                        // passing over this also allows for multiple of the same-named modules
733
                                        // to be loaded in junit tests that are run within one session
734
                                        log.debug("Got an error when trying to set the global property on module startup", e);
×
735
                                }
1✔
736
                                
737
                                // (this must be done after putting the module in the started
738
                                // list)
739
                                // if this module defined any privileges or global properties,
740
                                // make sure they are added to the database
741
                                // (Unfortunately, placing the call here will duplicate work
742
                                // done at initial app startup)
743
                                if (!module.getPrivileges().isEmpty() || !module.getGlobalProperties().isEmpty()) {
1✔
744
                                        log.debug("Updating core dataset");
×
745
                                        Context.checkCoreDataset();
×
746
                                        // checkCoreDataset() currently doesn't throw an error. If
747
                                        // it did, it needs to be
748
                                        // caught and the module needs to be stopped and given a
749
                                        // startup error
750
                                }
751
                                
752
                                // should be near the bottom so the module has all of its stuff
753
                                // set up for it already.
754
                                try {
755
                                        if (module.getModuleActivator() != null) {
1✔
756
                                                // if extends BaseModuleActivator
757
                                                module.getModuleActivator().willStart();
1✔
758
                                        }
759
                                }
760
                                catch (ModuleException e) {
×
761
                                        // just rethrow module exceptions. This should be used for a
762
                                        // module marking that it had trouble starting
763
                                        throw e;
×
764
                                }
765
                                catch (Exception e) {
×
766
                                        throw new ModuleException("Error while calling module's Activator.startup()/willStart() method", e);
×
767
                                }
1✔
768
                                
769
                                // erase any previous startup error
770
                                module.clearStartupError();
1✔
771
                        }
772
                        catch (Exception e) {
×
773
                                log.warn("Error while trying to start module: " + moduleId, e);
×
774
                                module.setStartupErrorMessage("Error while trying to start module", e);
×
775
                                notifySuperUsersAboutModuleFailure(module);
×
776
                                // undo all of the actions in startup
777
                                try {
778
                                        boolean skipOverStartedProperty = false;
×
779
                                        
780
                                        if (e instanceof ModuleMustStartException) {
×
781
                                                skipOverStartedProperty = true;
×
782
                                        }
783
                                        
784
                                        stopModule(module, skipOverStartedProperty, true);
×
785
                                }
786
                                catch (Exception e2) {
×
787
                                        // this will probably occur about the same place as the
788
                                        // error in startup
789
                                        log.debug("Error while stopping module: " + moduleId, e2);
×
790
                                }
×
791
                        }
1✔
792
                        
793
                }
794
                
795
                if (applicationContext != null) {
1✔
796
                        ModuleUtil.refreshApplicationContext(applicationContext, isOpenmrsStartup, module);
×
797
                }
798
                
799
                return module;
1✔
800
        }
801
        
802
        private static void registerProvidedPackages(ModuleClassLoader moduleClassLoader) {
803
                for (String providedPackage : moduleClassLoader.getProvidedPackages()) {
1✔
804
                        Set<ModuleClassLoader> newSet = new HashSet<>();
1✔
805
                        
806
                        Set<ModuleClassLoader> set = providedPackages.get(providedPackage);
1✔
807
                        if (set != null) {
1✔
808
                                newSet.addAll(set);
1✔
809
                        }
810
                        
811
                        newSet.add(moduleClassLoader);
1✔
812
                        providedPackages.put(providedPackage, newSet);
1✔
813
                }
1✔
814
        }
1✔
815
        
816
        private static void unregisterProvidedPackages(ModuleClassLoader moduleClassLoader) {
817
                for (String providedPackage : moduleClassLoader.getProvidedPackages()) {
1✔
818
                        Set<ModuleClassLoader> newSet = new HashSet<>();
1✔
819
                        
820
                        Set<ModuleClassLoader> set = providedPackages.get(providedPackage);
1✔
821
                        if (set != null) {
1✔
822
                                newSet.addAll(set);
1✔
823
                        }
824
                        newSet.remove(moduleClassLoader);
1✔
825
                        
826
                        providedPackages.put(providedPackage, newSet);
1✔
827
                }
1✔
828
        }
1✔
829
        
830
        public static Set<ModuleClassLoader> getModuleClassLoadersForPackage(String packageName) {
831
                Set<ModuleClassLoader> set = providedPackages.get(packageName);
1✔
832
                if (set == null) {
1✔
833
                        return Collections.emptySet();
1✔
834
                } else {
835
                        return new HashSet<>(set);
1✔
836
                }
837
        }
838
        
839
        /**
840
         * Gets the error message of a module which fails to start.
841
         *
842
         * @param module the module that has failed to start.
843
         * @return the message text.
844
         */
845
        private static String getFailedToStartModuleMessage(Module module) {
846
                String[] params = { module.getName(), String.join(",", getMissingRequiredModules(module)) };
×
847
                return Context.getMessageSourceService().getMessage("Module.error.moduleCannotBeStarted", params,
×
848
                        Context.getLocale());
×
849
        }
850
        
851
        /**
852
         * Gets the error message of cyclic dependencies between modules
853
         *
854
         * @return the message text.
855
         */
856
        private static String getCyclicDependenciesMessage(String message) {
857
                return Context.getMessageSourceService().getMessage("Module.error.cyclicDependencies", new Object[] { message },
×
858
                        Context.getLocale());
×
859
        }
860
        
861
        /**
862
         * Loop over the given module's advice objects and load them into the Context This needs to be
863
         * called for all started modules after every restart of the Spring Application Context
864
         *
865
         * @param module
866
         */
867
        public static void loadAdvice(Module module) {
868
                for (AdvicePoint advice : module.getAdvicePoints()) {
×
869
                        Class<?> cls;
870
                        try {
871
                                cls = Context.loadClass(advice.getPoint());
×
872
                                Object aopObject = advice.getClassInstance();
×
873
                                if (aopObject instanceof Advisor) {
×
874
                                        log.debug("adding advisor [{}]", aopObject.getClass());
×
875
                                        Context.addAdvisor(cls, (Advisor) aopObject);
×
876
                                } else if (aopObject != null) {
×
877
                                        log.debug("adding advice [{}]", aopObject.getClass());
×
878
                                        Context.addAdvice(cls, (Advice) aopObject);
×
879
                                } else {
880
                                        log.debug("Could not load advice class for {} [{}]", advice.getPoint(), advice.getClassName());
×
881
                                }
882
                        }
883
                        catch (ClassNotFoundException | NoClassDefFoundError e) {
×
884
                                log.warn("Could not load advice point [{}]", advice.getPoint(), e);
×
885
                        }
×
886
                }
×
887
        }
×
888
        
889
        /**
890
         * Execute the given sql diff section for the given module
891
         *
892
         * @param module the module being executed on
893
         * @param version the version of this sql diff
894
         * @param sql the actual sql statements to run (separated by semi colons)
895
         */
896
        private static void runDiff(Module module, String version, String sql) {
897
                AdministrationService as = Context.getAdministrationService();
×
898
                
899
                String key = module.getModuleId() + ".database_version";
×
900
                GlobalProperty gp = as.getGlobalPropertyObject(key);
×
901
                
902
                boolean executeSQL = false;
×
903
                
904
                // check given version against current version
905
                if (gp != null && StringUtils.hasLength(gp.getPropertyValue())) {
×
906
                        String currentDbVersion = gp.getPropertyValue();
×
907
                        if (log.isDebugEnabled()) {
×
908
                                log.debug("version:column {}:{}", version, currentDbVersion);
×
909
                                log.debug("compare: {}", ModuleUtil.compareVersion(version, currentDbVersion));
×
910
                        }
911
                        if (ModuleUtil.compareVersion(version, currentDbVersion) > 0) {
×
912
                                executeSQL = true;
×
913
                        }
914
                } else {
×
915
                        executeSQL = true;
×
916
                }
917
                
918
                // version is greater than the currently installed version. execute this update.
919
                if (executeSQL) {
×
920
                        try {
921
                                Context.addProxyPrivilege(PrivilegeConstants.SQL_LEVEL_ACCESS);
×
922
                                log.debug("Executing sql: " + sql);
×
923
                                String[] sqlStatements = sql.split(";");
×
924
                                for (String sqlStatement : sqlStatements) {
×
925
                                        if (sqlStatement.trim().length() > 0) {
×
926
                                                as.executeSQL(sqlStatement, false);
×
927
                                        }
928
                                }
929
                        }
930
                        finally {
931
                                Context.removeProxyPrivilege(PrivilegeConstants.SQL_LEVEL_ACCESS);
×
932
                        }
933
                        
934
                        // save the global property
935
                        try {
936
                                Context.addProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);
×
937
                                
938
                                String description = "DO NOT MODIFY.  Current database version number for the " + module.getModuleId()
×
939
                                        + " module.";
940
                                
941
                                if (gp == null) {
×
942
                                        log.info("Global property " + key + " was not found. Creating one now.");
×
943
                                        gp = new GlobalProperty(key, version, description);
×
944
                                        as.saveGlobalProperty(gp);
×
945
                                } else if (!gp.getPropertyValue().equals(version)) {
×
946
                                        log.info("Updating global property " + key + " to version: " + version);
×
947
                                        gp.setDescription(description);
×
948
                                        gp.setPropertyValue(version);
×
949
                                        as.saveGlobalProperty(gp);
×
950
                                } else {
951
                                        log.error("Should not be here. GP property value and sqldiff version should not be equal");
×
952
                                }
953
                                
954
                        }
955
                        finally {
956
                                Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);
×
957
                        }
958
                        
959
                }
960
                
961
        }
×
962
        
963
        /**
964
         * Execute all not run changeSets in liquibase.xml for the given module
965
         *
966
         * @param module the module being executed on
967
         */
968
        private static void runLiquibase(Module module) {
969
                ModuleClassLoader moduleClassLoader = getModuleClassLoader(module);
1✔
970
                boolean liquibaseFileExists = false;
1✔
971
                
972
                if (moduleClassLoader != null) {
1✔
973
                        try (InputStream inStream = moduleClassLoader.getResourceAsStream(MODULE_CHANGELOG_FILENAME)) {
1✔
974
                                liquibaseFileExists = (inStream != null);
1✔
975
                        }
976
                        catch (IOException ignored) {
×
977
                                
978
                        }
1✔
979
                }
980
                
981
                if (liquibaseFileExists) {
1✔
982
                        try {
983
                                // run liquibase.xml by Liquibase API
984
                                DatabaseUpdater.executeChangelog(MODULE_CHANGELOG_FILENAME, new Contexts(), null, moduleClassLoader);
×
985
                        }
986
                        catch (InputRequiredException e) {
×
987
                                // the user would be stepped through the questions returned here.
988
                                throw new ModuleException("Input during database updates is not yet implemented.", module.getName(), e);
×
989
                        }
990
                        catch (Exception e) {
×
991
                                throw new ModuleException("Unable to update data model using " + MODULE_CHANGELOG_FILENAME + ".",
×
992
                                        module.getName(), e);
×
993
                        }
×
994
                }
995
        }
1✔
996
        
997
        /**
998
         * Runs through the advice and extension points and removes from api. <br>
999
         * Also calls mod.Activator.shutdown()
1000
         *
1001
         * @param mod module to stop
1002
         * @see ModuleFactory#stopModule(Module, boolean, boolean)
1003
         */
1004
        public static void stopModule(Module mod) {
1005
                stopModule(mod, false, false);
1✔
1006
        }
1✔
1007
        
1008
        /**
1009
         * Runs through the advice and extension points and removes from api.<br>
1010
         * Also calls mod.Activator.shutdown()
1011
         *
1012
         * @param mod the module to stop
1013
         * @param isShuttingDown true if this is called during the process of shutting down openmrs
1014
         * @see #stopModule(Module, boolean, boolean)
1015
         */
1016
        public static void stopModule(Module mod, boolean isShuttingDown) {
1017
                stopModule(mod, isShuttingDown, false);
1✔
1018
        }
1✔
1019
        
1020
        /**
1021
         * Runs through the advice and extension points and removes from api.<br>
1022
         * <code>skipOverStartedProperty</code> should only be true when openmrs is stopping modules because
1023
         * it is shutting down. When normally stopping a module, use {@link #stopModule(Module)} (or leave
1024
         * value as false). This property controls whether the globalproperty is set for startup/shutdown.
1025
         * <br>
1026
         * Also calls module's {@link ModuleActivator#stopped()}
1027
         *
1028
         * @param mod module to stop
1029
         * @param skipOverStartedProperty true if we don't want to set &lt;moduleid&gt;.started to false
1030
         * @param isFailedStartup true if this is being called as a cleanup because of a failed module
1031
         *            startup
1032
         * @return list of dependent modules that were stopped because this module was stopped. This will
1033
         *         never be null.
1034
         */
1035
        public static List<Module> stopModule(Module mod, boolean skipOverStartedProperty, boolean isFailedStartup)
1036
                throws ModuleMustStartException {
1037
                
1038
                List<Module> dependentModulesStopped = new ArrayList<>();
1✔
1039
                
1040
                if (mod != null) {
1✔
1041
                        
1042
                        if (!ModuleFactory.isModuleStarted(mod)) {
1✔
1043
                                return dependentModulesStopped;
×
1044
                        }
1045
                        
1046
                        try {
1047
                                // if extends BaseModuleActivator
1048
                                if (mod.getModuleActivator() != null) {
1✔
1049
                                        mod.getModuleActivator().willStop();
1✔
1050
                                }
1051
                        }
1052
                        catch (Exception t) {
×
1053
                                log.warn("Unable to call module's Activator.willStop() method", t);
×
1054
                        }
1✔
1055
                        
1056
                        String moduleId = mod.getModuleId();
1✔
1057
                        
1058
                        // don't allow mandatory modules to be stopped
1059
                        // don't use database checks here because spring might be in a bad state
1060
                        if (!isFailedStartup && mod.isMandatory()) {
1✔
1061
                                throw new MandatoryModuleException(moduleId);
×
1062
                        }
1063
                        
1064
                        if (!isFailedStartup && ModuleConstants.CORE_MODULES.containsKey(moduleId)) {
1✔
1065
                                throw new OpenmrsCoreModuleException(moduleId);
×
1066
                        }
1067
                        
1068
                        String modulePackage = mod.getPackageName();
1✔
1069
                        
1070
                        // stop all dependent modules
1071
                        // copy modules to new list to avoid "concurrent modification exception"
1072
                        List<Module> startedModulesCopy = new ArrayList<>(getStartedModules());
1✔
1073
                        for (Module dependentModule : startedModulesCopy) {
1✔
1074
                                if (dependentModule != null && !dependentModule.equals(mod)
1✔
1075
                                        && isModuleRequiredByAnother(dependentModule, modulePackage)) {
1✔
1076
                                        dependentModulesStopped.add(dependentModule);
1✔
1077
                                        dependentModulesStopped.addAll(stopModule(dependentModule, skipOverStartedProperty, isFailedStartup));
1✔
1078
                                }
1079
                        }
1✔
1080
                        
1081
                        getStartedModulesMap().remove(moduleId);
1✔
1082
                        if (actualStartupOrder != null) {
1✔
1083
                                actualStartupOrder.remove(moduleId);
1✔
1084
                                for (Module depModule : dependentModulesStopped) {
1✔
1085
                                        actualStartupOrder.remove(depModule.getModuleId());
1✔
1086
                                }
1✔
1087
                        }
1088
                        
1089
                        if (!skipOverStartedProperty && !Context.isRefreshingContext()) {
1✔
1090
                                saveGlobalProperty(moduleId + ".started", "false", getGlobalPropertyStartedDescription(moduleId));
1✔
1091
                        }
1092
                        
1093
                        ModuleClassLoader moduleClassLoader = getModuleClassLoaderMap().get(mod);
1✔
1094
                        if (moduleClassLoader != null) {
1✔
1095
                                unregisterProvidedPackages(moduleClassLoader);
1✔
1096
                                
1097
                                log.debug("Mod was in classloader map.  Removing advice and extensions.");
1✔
1098
                                // remove all advice by this module
1099
                                try {
1100
                                        for (AdvicePoint advice : mod.getAdvicePoints()) {
1✔
1101
                                                Class cls;
1102
                                                try {
1103
                                                        cls = Context.loadClass(advice.getPoint());
×
1104
                                                        Object aopObject = advice.getClassInstance();
×
1105
                                                        if (aopObject instanceof Advisor) {
×
1106
                                                                log.debug("adding advisor: " + aopObject.getClass());
×
1107
                                                                Context.removeAdvisor(cls, (Advisor) aopObject);
×
1108
                                                        } else {
1109
                                                                log.debug("Adding advice: " + aopObject.getClass());
×
1110
                                                                Context.removeAdvice(cls, (Advice) aopObject);
×
1111
                                                        }
1112
                                                }
1113
                                                catch (Exception t) {
×
1114
                                                        log.warn("Could not remove advice point: " + advice.getPoint(), t);
×
1115
                                                }
×
1116
                                        }
×
1117
                                }
1118
                                catch (Exception t) {
×
1119
                                        log.warn("Error while getting advicePoints from module: " + moduleId, t);
×
1120
                                }
1✔
1121
                                
1122
                                // remove all extensions by this module
1123
                                try {
1124
                                        for (Extension ext : mod.getExtensions()) {
1✔
1125
                                                String extId = ext.getExtensionId();
×
1126
                                                try {
1127
                                                        List<Extension> tmpExtensions = getExtensions(extId);
×
1128
                                                        tmpExtensions.remove(ext);
×
1129
                                                        getExtensionMap().put(extId, tmpExtensions);
×
1130
                                                }
1131
                                                catch (Exception exterror) {
×
1132
                                                        log.warn("Error while getting extension: " + ext, exterror);
×
1133
                                                }
×
1134
                                        }
×
1135
                                }
1136
                                catch (Exception t) {
×
1137
                                        log.warn("Error while getting extensions from module: " + moduleId, t);
×
1138
                                }
1✔
1139
                        }
1140
                        
1141
                        //Run the onShutdown() method for openmrs services in this module.
1142
                        List<OpenmrsService> services = Context.getModuleOpenmrsServices(modulePackage);
1✔
1143
                        if (services != null) {
1✔
1144
                                for (OpenmrsService service : services) {
1✔
1145
                                        service.onShutdown();
1✔
1146
                                }
1✔
1147
                        }
1148
                        
1149
                        try {
1150
                                if (mod.getModuleActivator() != null) {// extends BaseModuleActivator
1✔
1151
                                        mod.getModuleActivator().stopped();
1✔
1152
                                }
1153
                        }
1154
                        catch (Exception t) {
×
1155
                                log.warn("Unable to call module's Activator.shutdown() method", t);
×
1156
                        }
1✔
1157
                        
1158
                        //Since extensions are loaded by the module class loader which is about to be disposed,
1159
                        //we need to clear them, else we shall never be able to unload the class loader until
1160
                        //when we unload the module, hence resulting into two problems:
1161
                        // 1) Memory leakage for start/stop module.
1162
                        // 2) Calls to Context.getService(Service.class) which are made within these extensions 
1163
                        //          will throw APIException("Service not found: ") because their calls to Service.class
1164
                        //    will pass in a Class from the old module class loader (which loaded them) yet the
1165
                        //    ServiceContext will have new services from a new module class loader.
1166
                        //
1167
                        //Same thing applies to activator, moduleActivator and AdvicePoint classInstance.
1168
                        mod.getExtensions().clear();
1✔
1169
                        mod.setModuleActivator(null);
1✔
1170
                        mod.disposeAdvicePointsClassInstance();
1✔
1171
                        
1172
                        ModuleClassLoader cl = removeClassLoader(mod);
1✔
1173
                        if (cl != null) {
1✔
1174
                                cl.dispose();
1✔
1175
                                // remove files from lib cache
1176
                                File folder = OpenmrsClassLoader.getLibCacheFolder();
1✔
1177
                                File tmpModuleDir = new File(folder, moduleId);
1✔
1178
                                try {
1179
                                        OpenmrsUtil.deleteDirectory(tmpModuleDir);
1✔
1180
                                }
1181
                                catch (IOException e) {
×
1182
                                        log.warn("Unable to delete libcachefolder for " + moduleId);
×
1183
                                }
1✔
1184
                        }
1185
                }
1186
                
1187
                return dependentModulesStopped;
1✔
1188
        }
1189
        
1190
        /**
1191
         * Checks if a module is required by another
1192
         *
1193
         * @param dependentModule the module whose required modules are to be checked
1194
         * @param modulePackage the package of the module to check if required by another
1195
         * @return true if the module is required, else false
1196
         */
1197
        private static boolean isModuleRequiredByAnother(Module dependentModule, String modulePackage) {
1198
                return dependentModule.getRequiredModules() != null && dependentModule.getRequiredModules().contains(modulePackage);
1✔
1199
        }
1200
        
1201
        private static ModuleClassLoader removeClassLoader(Module mod) {
1202
                // create map if it is null
1203
                ModuleClassLoader cl = moduleClassLoaders.getIfPresent(mod);
1✔
1204
                if (cl == null) {
1✔
1205
                        log.warn("Module: " + mod.getModuleId() + " does not exist");
×
1206
                }
1207
                
1208
                moduleClassLoaders.invalidate(mod);
1✔
1209
                
1210
                return cl;
1✔
1211
        }
1212
        
1213
        /**
1214
         * Removes module from module repository
1215
         *
1216
         * @param mod module to unload
1217
         */
1218
        public static void unloadModule(Module mod) {
1219
                
1220
                // remove this module's advice and extensions
1221
                if (isModuleStarted(mod)) {
1✔
1222
                        stopModule(mod, true);
1✔
1223
                }
1224
                
1225
                // remove from list of loaded modules
1226
                getLoadedModules().remove(mod);
1✔
1227
                
1228
                if (mod != null) {
1✔
1229
                        // remove the file from the module repository
1230
                        File file = mod.getFile();
1✔
1231
                        
1232
                        boolean deleted = file.delete();
1✔
1233
                        if (!deleted) {
1✔
1234
                                file.deleteOnExit();
×
1235
                                log.warn("Could not delete " + file.getAbsolutePath());
×
1236
                        }
1237
                        
1238
                }
1239
        }
1✔
1240
        
1241
        /**
1242
         * Return all of the extensions associated with the given <code>pointId</code> Returns empty
1243
         * extension list if no modules extend this pointId
1244
         *
1245
         * @param pointId
1246
         * @return List of extensions
1247
         */
1248
        public static List<Extension> getExtensions(String pointId) {
1249
                List<Extension> extensions;
1250
                Map<String, List<Extension>> extensionMap = getExtensionMap();
×
1251
                
1252
                // get all extensions for this exact pointId
1253
                extensions = extensionMap.get(pointId);
×
1254
                if (extensions == null) {
×
1255
                        extensions = new ArrayList<>();
×
1256
                }
1257
                
1258
                // if this pointId doesn't contain the separator character, search
1259
                // for this point prepended with each MEDIA TYPE
1260
                if (!pointId.contains(Extension.EXTENSION_ID_SEPARATOR)) {
×
1261
                        for (MEDIA_TYPE mediaType : Extension.MEDIA_TYPE.values()) {
×
1262
                                
1263
                                // get all extensions for this type and point id
1264
                                List<Extension> tmpExtensions = extensionMap.get(Extension.toExtensionId(pointId, mediaType));
×
1265
                                
1266
                                // 'extensions' should be a unique list
1267
                                if (tmpExtensions != null) {
×
1268
                                        for (Extension ext : tmpExtensions) {
×
1269
                                                if (!extensions.contains(ext)) {
×
1270
                                                        extensions.add(ext);
×
1271
                                                }
1272
                                        }
×
1273
                                }
1274
                        }
1275
                }
1276
                
1277
                log.debug("Getting extensions defined by : " + pointId);
×
1278
                return extensions;
×
1279
        }
1280
        
1281
        /**
1282
         * Return all of the extensions associated with the given <code>pointId</code> Returns
1283
         * getExtension(pointId) if no modules extend this pointId for given media type
1284
         *
1285
         * @param pointId
1286
         * @param type Extension.MEDIA_TYPE
1287
         * @return List of extensions
1288
         */
1289
        public static List<Extension> getExtensions(String pointId, Extension.MEDIA_TYPE type) {
1290
                String key = Extension.toExtensionId(pointId, type);
×
1291
                List<Extension> extensions = getExtensionMap().get(key);
×
1292
                if (extensions != null) {
×
1293
                        log.debug("Getting extensions defined by : " + key);
×
1294
                        return extensions;
×
1295
                } else {
1296
                        return getExtensions(pointId);
×
1297
                }
1298
        }
1299
        
1300
        /**
1301
         * Get a list of required Privileges defined by the modules
1302
         *
1303
         * @return <code>List&lt;Privilege&gt;</code> of the required privileges
1304
         */
1305
        public static List<Privilege> getPrivileges() {
1306
                
1307
                List<Privilege> privileges = new ArrayList<>();
1✔
1308
                
1309
                for (Module mod : getStartedModules()) {
1✔
1310
                        privileges.addAll(mod.getPrivileges());
×
1311
                }
×
1312
                
1313
                log.debug(privileges.size() + " new privileges");
1✔
1314
                
1315
                return privileges;
1✔
1316
        }
1317
        
1318
        /**
1319
         * Get a list of required GlobalProperties defined by the modules
1320
         *
1321
         * @return <code>List&lt;GlobalProperty&gt;</code> object of the module's global properties
1322
         */
1323
        public static List<GlobalProperty> getGlobalProperties() {
1324
                
1325
                List<GlobalProperty> globalProperties = new ArrayList<>();
×
1326
                
1327
                for (Module mod : getStartedModules()) {
×
1328
                        globalProperties.addAll(mod.getGlobalProperties());
×
1329
                }
×
1330
                
1331
                log.debug(globalProperties.size() + " new global properties");
×
1332
                
1333
                return globalProperties;
×
1334
        }
1335
        
1336
        /**
1337
         * Checks whether the given module is activated
1338
         *
1339
         * @param mod Module to check
1340
         * @return true if the module is started, false otherwise
1341
         */
1342
        public static boolean isModuleStarted(Module mod) {
1343
                return getStartedModulesMap().containsValue(mod);
1✔
1344
        }
1345
        
1346
        /**
1347
         * Checks whether the given module, identified by its id, is started.
1348
         *
1349
         * @param moduleId module id. e.g formentry, logic
1350
         * @since 1.9
1351
         * @return true if the module is started, false otherwise
1352
         */
1353
        public static boolean isModuleStarted(String moduleId) {
1354
                return getStartedModulesMap().containsKey(moduleId);
1✔
1355
        }
1356
        
1357
        /**
1358
         * Get a module's classloader
1359
         *
1360
         * @param mod Module to fetch the class loader for
1361
         * @return ModuleClassLoader pertaining to this module. Returns null if the module is not started
1362
         * @throws ModuleException if the module does not have a registered classloader
1363
         */
1364
        public static ModuleClassLoader getModuleClassLoader(Module mod) throws ModuleException {
1365
                ModuleClassLoader mcl = getModuleClassLoaderMap().get(mod);
1✔
1366
                
1367
                if (mcl == null) {
1✔
1368
                        log.debug("Module classloader not found for module with id: " + mod.getModuleId());
1✔
1369
                }
1370
                
1371
                return mcl;
1✔
1372
        }
1373
        
1374
        /**
1375
         * Get a module's classloader via the module id
1376
         *
1377
         * @param moduleId <code>String</code> id of the module
1378
         * @return ModuleClassLoader pertaining to this module. Returns null if the module is not started
1379
         * @throws ModuleException if this module isn't started or doesn't have a classloader
1380
         * @see #getModuleClassLoader(Module)
1381
         */
1382
        public static ModuleClassLoader getModuleClassLoader(String moduleId) throws ModuleException {
1383
                Module mod = getStartedModulesMap().get(moduleId);
×
1384
                if (mod == null) {
×
1385
                        log.debug("Module id not found in list of started modules: " + moduleId);
×
1386
                }
1387
                
1388
                return getModuleClassLoader(mod);
×
1389
        }
1390
        
1391
        /**
1392
         * Returns all module classloaders This method will not return null
1393
         *
1394
         * @return Collection&lt;ModuleClassLoader&gt; all known module classloaders or empty list.
1395
         */
1396
        public static Collection<ModuleClassLoader> getModuleClassLoaders() {
1397
                Map<Module, ModuleClassLoader> classLoaders = getModuleClassLoaderMap();
1✔
1398
                if (classLoaders.size() > 0) {
1✔
1399
                        return classLoaders.values();
1✔
1400
                }
1401
                
1402
                return Collections.emptyList();
1✔
1403
        }
1404
        
1405
        /**
1406
         * Return all current classloaders keyed on module object
1407
         *
1408
         * @return Map&lt;Module, ModuleClassLoader&gt;
1409
         */
1410
        public static Map<Module, ModuleClassLoader> getModuleClassLoaderMap() {
1411
                // because the OpenMRS classloader depends on this static function, it is weirdly possible for this to get called
1412
                // as this classfile is loaded, in which case, the static final field can be null.
1413
                if (moduleClassLoaders == null) {
1✔
1414
                        return Collections.emptyMap();
×
1415
                }
1416
                
1417
                return moduleClassLoaders.asMap();
1✔
1418
        }
1419
        
1420
        /**
1421
         * Return the current extension map keyed on extension point id
1422
         *
1423
         * @return Map&lt;String, List&lt;Extension&gt;&gt;
1424
         */
1425
        public static Map<String, List<Extension>> getExtensionMap() {
1426
                return extensionMap;
×
1427
        }
1428
        
1429
        /**
1430
         * Tests whether all modules mentioned in module.requiredModules are loaded and started already (by
1431
         * being in the startedModules list)
1432
         *
1433
         * @param module
1434
         * @return true/false boolean whether this module's required modules are all started
1435
         */
1436
        private static boolean requiredModulesStarted(Module module) {
1437
                //required
1438
                for (String reqModPackage : module.getRequiredModules()) {
1✔
1439
                        boolean started = false;
1✔
1440
                        for (Module mod : getStartedModules()) {
1✔
1441
                                if (mod.getPackageName().equals(reqModPackage)) {
1✔
1442
                                        String reqVersion = module.getRequiredModuleVersion(reqModPackage);
1✔
1443
                                        if (reqVersion == null || ModuleUtil.compareVersion(mod.getVersion(), reqVersion) >= 0) {
1✔
1444
                                                started = true;
1✔
1445
                                        }
1446
                                        break;
1447
                                }
1448
                        }
×
1449
                        
1450
                        if (!started) {
1✔
1451
                                return false;
1✔
1452
                        }
1453
                }
1✔
1454
                
1455
                return true;
1✔
1456
        }
1457
        
1458
        /**
1459
         * Update the module: 1) Download the new module 2) Unload the old module 3) Load/start the new
1460
         * module
1461
         *
1462
         * @param mod
1463
         */
1464
        public static Module updateModule(Module mod) throws ModuleException {
1465
                if (mod.getDownloadURL() == null) {
×
1466
                        return mod;
×
1467
                }
1468
                
1469
                URL url;
1470
                try {
1471
                        url = new URL(mod.getDownloadURL());
×
1472
                }
1473
                catch (MalformedURLException e) {
×
1474
                        throw new ModuleException("Unable to download module update", e);
×
1475
                }
×
1476
                
1477
                unloadModule(mod);
×
1478
                
1479
                // copy content to a temporary file
1480
                InputStream inputStream = ModuleUtil.getURLStream(url);
×
1481
                log.warn("url pathname: " + url.getPath());
×
1482
                String filename = url.getPath().substring(url.getPath().lastIndexOf("/"));
×
1483
                File moduleFile = ModuleUtil.insertModuleFile(inputStream, filename);
×
1484
                
1485
                try {
1486
                        // load, and start the new module
1487
                        Module newModule = loadModule(moduleFile);
×
1488
                        startModule(newModule);
×
1489
                        return newModule;
×
1490
                }
1491
                catch (Exception e) {
×
1492
                        log.warn("Error while unloading old module and loading in new module");
×
1493
                        moduleFile.delete();
×
1494
                        return mod;
×
1495
                }
1496
                
1497
        }
1498
        
1499
        /**
1500
         * Validates the given token.
1501
         * <p>
1502
         * It is thread safe.
1503
         *
1504
         * @param token
1505
         * @since 1.9.2
1506
         */
1507
        public static boolean isTokenValid(DaemonToken token) {
1508
                if (token == null) {
×
1509
                        return false;
×
1510
                } else {
1511
                        //We need to synchronize to guarantee that the last passed token is valid.
1512
                        synchronized (daemonTokens) {
×
1513
                                DaemonToken validToken = daemonTokens.getIfPresent(token.getId());
×
1514
                                //Compare by reference to defend from overridden equals.
1515
                                return validToken != null && validToken == token;
×
1516
                        }
1517
                }
1518
        }
1519
        
1520
        /**
1521
         * Passes a daemon token to the given module.
1522
         * <p>
1523
         * The token is passed to that module's {@link ModuleActivator} if it implements
1524
         * {@link DaemonTokenAware}.
1525
         * <p>
1526
         * This method is called automatically before {@link ModuleActivator#contextRefreshed()} or
1527
         * {@link ModuleActivator#started()}. Note that it may be called multiple times and there is no
1528
         * guarantee that it will always pass the same token. The last passed token is valid, whereas
1529
         * previously passed tokens may be invalidated.
1530
         * <p>
1531
         * It is thread safe.
1532
         *
1533
         * @param module
1534
         * @since 1.9.2
1535
         */
1536
        static void passDaemonToken(Module module) {
1537
                ModuleActivator moduleActivator = module.getModuleActivator();
×
1538
                if (moduleActivator instanceof DaemonTokenAware) {
×
1539
                        DaemonToken daemonToken = getDaemonToken(module);
×
1540
                        ((DaemonTokenAware) module.getModuleActivator()).setDaemonToken(daemonToken);
×
1541
                }
1542
        }
×
1543
        
1544
        /**
1545
         * Gets a new or existing token. Uses weak references for tokens so that they are garbage collected
1546
         * when not needed.
1547
         * <p>
1548
         * It is thread safe.
1549
         *
1550
         * @param module
1551
         * @return the token
1552
         */
1553
        private static DaemonToken getDaemonToken(Module module) {
1554
                DaemonToken token;
1555
                try {
1556
                        token = daemonTokens.get(module.getModuleId(), () -> new DaemonToken(module.getModuleId()));
×
1557
                }
1558
                catch (ExecutionException e) {
×
1559
                        throw new APIException(e);
×
1560
                }
×
1561
                
1562
                return token;
×
1563
        }
1564
        
1565
        /**
1566
         * Returns the description for the [moduleId].started global property
1567
         *
1568
         * @param moduleId
1569
         * @return description to use for the .started property
1570
         */
1571
        private static String getGlobalPropertyStartedDescription(String moduleId) {
1572
                String ret = "DO NOT MODIFY. true/false whether or not the " + moduleId;
1✔
1573
                ret += " module has been started.  This is used to make sure modules that were running ";
1✔
1574
                ret += " prior to a restart are started again";
1✔
1575
                
1576
                return ret;
1✔
1577
        }
1578
        
1579
        /**
1580
         * Returns the description for the [moduleId].mandatory global property
1581
         *
1582
         * @param moduleId
1583
         * @return description to use for .mandatory property
1584
         */
1585
        private static String getGlobalPropertyMandatoryModuleDescription(String moduleId) {
1586
                String ret = "true/false whether or not the " + moduleId;
1✔
1587
                ret += " module MUST start when openmrs starts.  This is used to make sure that mission critical";
1✔
1588
                ret += " modules are always running if openmrs is running.";
1✔
1589
                
1590
                return ret;
1✔
1591
        }
1592
        
1593
        /**
1594
         * Convenience method to save a global property with the given value. Proxy privileges are added so
1595
         * that this can occur at startup.
1596
         *
1597
         * @param key the property for this global property
1598
         * @param value the value for this global property
1599
         * @param desc the description
1600
         * @see AdministrationService#saveGlobalProperty(GlobalProperty)
1601
         */
1602
        private static void saveGlobalProperty(String key, String value, String desc) {
1603
                try {
1604
                        AdministrationService as = Context.getAdministrationService();
1✔
1605
                        GlobalProperty gp = as.getGlobalPropertyObject(key);
1✔
1606
                        if (gp == null) {
1✔
1607
                                gp = new GlobalProperty(key, value, desc);
1✔
1608
                        } else {
1609
                                gp.setPropertyValue(value);
1✔
1610
                        }
1611
                        
1612
                        as.saveGlobalProperty(gp);
1✔
1613
                }
1614
                catch (Exception e) {
×
1615
                        log.warn("Unable to save the global property", e);
×
1616
                }
1✔
1617
        }
1✔
1618
        
1619
        /**
1620
         * Convenience method used to identify module interdependencies and alert the user before modules
1621
         * are shut down.
1622
         *
1623
         * @param moduleId the moduleId used to identify the module being validated
1624
         * @return List&lt;dependentModules&gt; the list of moduleId's which depend on the module about to
1625
         *         be shutdown.
1626
         * @since 1.10
1627
         */
1628
        public static List<String> getDependencies(String moduleId) {
1629
                List<String> dependentModules = null;
×
1630
                Module module = getModuleById(moduleId);
×
1631
                
1632
                Map<String, Module> startedModules = getStartedModulesMap();
×
1633
                String modulePackage = module.getPackageName();
×
1634
                
1635
                for (Entry<String, Module> entry : startedModules.entrySet()) {
×
1636
                        if (!moduleId.equals(entry.getKey()) && entry.getValue().getRequiredModules().contains(modulePackage)) {
×
1637
                                if (dependentModules == null) {
×
1638
                                        dependentModules = new ArrayList<>();
×
1639
                                }
1640
                                dependentModules.add(entry.getKey() + " " + entry.getValue().getVersion());
×
1641
                        }
1642
                }
×
1643
                return dependentModules;
×
1644
        }
1645
}
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

© 2025 Coveralls, Inc