/
ModuleFactory.java
1729 lines (1508 loc) · 56.2 KB
/
ModuleFactory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.Vector;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.aopalliance.aop.Advice;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.GlobalProperty;
import org.openmrs.Privilege;
import org.openmrs.api.AdministrationService;
import org.openmrs.api.OpenmrsService;
import org.openmrs.api.context.Context;
import org.openmrs.api.context.Daemon;
import org.openmrs.module.Extension.MEDIA_TYPE;
import org.openmrs.util.CycleException;
import org.openmrs.util.DatabaseUpdateException;
import org.openmrs.util.DatabaseUpdater;
import org.openmrs.util.Graph;
import org.openmrs.util.InputRequiredException;
import org.openmrs.util.OpenmrsClassLoader;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.util.OpenmrsUtil;
import org.openmrs.util.PrivilegeConstants;
import org.springframework.aop.Advisor;
import org.springframework.context.support.AbstractRefreshableApplicationContext;
import org.springframework.util.StringUtils;
/**
* Methods for loading, starting, stopping, and storing OpenMRS modules
*/
public class ModuleFactory {
private static Log log = LogFactory.getLog(ModuleFactory.class);
protected static volatile Map<String, Module> loadedModules = new WeakHashMap<String, Module>();
protected static volatile Map<String, Module> startedModules = new WeakHashMap<String, Module>();
protected static volatile Map<String, List<Extension>> extensionMap = new HashMap<String, List<Extension>>();
// maps to keep track of the memory and objects to free/close
protected static volatile Map<Module, ModuleClassLoader> moduleClassLoaders = new WeakHashMap<Module, ModuleClassLoader>();
private static Map<String, Set<ModuleClassLoader>> providedPackages = new ConcurrentHashMap<String, Set<ModuleClassLoader>>();
// the name of the file within a module file
private static final String MODULE_CHANGELOG_FILENAME = "liquibase.xml";
private static final Map<String, DaemonToken> daemonTokens = new WeakHashMap<String, DaemonToken>();
private static volatile Set<String> actualStartupOrder;
/**
* Add a module (in the form of a jar file) to the list of openmrs modules Returns null if an
* error occurred and/or module was not successfully loaded
*
* @param moduleFile
* @return Module
*/
public static Module loadModule(File moduleFile) throws ModuleException {
return loadModule(moduleFile, true);
}
/**
* Add a module (in the form of a jar file) to the list of openmrs modules Returns null if an
* error occurred and/or module was not successfully loaded
*
* @param moduleFile
* @param replaceIfExists unload a module that has the same moduleId if one is loaded already
* @return Module
*/
public static Module loadModule(File moduleFile, Boolean replaceIfExists) throws ModuleException {
Module module = getModuleFromFile(moduleFile);
if (module != null) {
loadModule(module, replaceIfExists);
}
return module;
}
/**
* Add a module to the list of openmrs modules
*
* @param module
* @param replaceIfExists unload a module that has the same moduleId if one is loaded already
* @should load module if it is currently not loaded
* @should not load module if already loaded
* @should always load module if replacement is wanted
* @should not load an older version of the same module
* @should load a newer version of the same module
* @return module the module that was loaded or if the module exists already with the same
* version, the old module
*/
public static Module loadModule(Module module, Boolean replaceIfExists) throws ModuleException {
if (log.isDebugEnabled()) {
log.debug("Adding module " + module.getName() + " to the module queue");
}
Module oldModule = getLoadedModulesMap().get(module.getModuleId());
if (oldModule != null) {
int versionComparison = ModuleUtil.compareVersion(oldModule.getVersion(), module.getVersion());
if (versionComparison < 0) {
// if oldModule version is lower, unload it and use the new
unloadModule(oldModule);
} else if (versionComparison == 0) {
if (replaceIfExists) {
// if the versions are the same and we're told to replaceIfExists, use the new
unloadModule(oldModule);
} else {
// if the versions are equal and we're not told to replaceIfExists, jump out of here in a bad way
throw new ModuleException("A module with the same id and version already exists", module.getModuleId());
}
} else {
// if the older (already loaded) module is newer, keep that original one that was loaded. return that one.
return oldModule;
}
}
getLoadedModulesMap().put(module.getModuleId(), module);
return module;
}
/**
* Load OpenMRS modules from <code>OpenmrsUtil.getModuleRepository()</code>
*/
public static void loadModules() {
// load modules from the user's module repository directory
File modulesFolder = ModuleUtil.getModuleRepository();
if (log.isDebugEnabled()) {
log.debug("Loading modules from: " + modulesFolder.getAbsolutePath());
}
if (modulesFolder.isDirectory()) {
loadModules(Arrays.asList(modulesFolder.listFiles()));
} else {
log.error("modules folder: '" + modulesFolder.getAbsolutePath() + "' is not a valid directory");
}
}
/**
* Attempt to load the given files as OpenMRS modules
*
* @param modulesToLoad the list of files to try and load
* @should not crash when file is not found or broken
* @should setup requirement mappings for every module
* @should not start the loaded modules
*/
public static void loadModules(List<File> modulesToLoad) {
// loop over the modules and load all the modules that we can
for (File f : modulesToLoad) {
if (f.exists()) {
// ignore .svn folder and the like
if (!f.getName().startsWith(".")) {
try {
Module mod = loadModule(f, true); // last module loaded wins
log.debug("Loaded module: " + mod + " successfully");
} catch (Exception e) {
log.debug("Unable to load file in module directory: " + f + ". Skipping file.", e);
}
}
} else {
log.debug("Could not find file in module directory: " + f);
}
}
//inform modules, that they can't start before other modules
Map<String, Module> loadedModulesMap = getLoadedModulesMapPackage();
for (String key : loadedModules.keySet()) {
Module m = loadedModules.get(key);
Map<String, String> startBeforeModules = m.getStartBeforeModulesMap();
if (startBeforeModules.size() > 0) {
for (String s : startBeforeModules.keySet()) {
Module mod = loadedModulesMap.get(s);
if (mod != null) {
mod.addRequiredModule(m.getPackageName(), m.getVersion());
}
}
}
}
}
/**
* Try to start all of the loaded modules that have the global property <i>moduleId</i>.started
* is set to "true" or the property does not exist. Otherwise, leave it as only "loaded"<br>
* <br>
* Modules that are already started will be skipped.
*/
public static void startModules() {
// loop over and try starting each of the loaded modules
if (!getLoadedModules().isEmpty()) {
List<Module> modules = getModulesThatShouldStart();
try {
modules = getModulesInStartupOrder(modules);
}
catch (CycleException ex) {
String message = getCyclicDependenciesMessage(ex.getMessage());
log.error(message, ex);
notifySuperUsersAboutCyclicDependencies(ex);
modules = (List<Module>)ex.getExtraData();
}
// try and start the modules that should be started
for (Module mod : modules) {
if (mod.isStarted()) {
continue; // skip over modules that are already started
}
// Skip module if required ones are not started
if (!requiredModulesStarted(mod)) {
String message = getFailedToStartModuleMessage(mod);
log.error(message);
mod.setStartupErrorMessage(message);
notifySuperUsersAboutModuleFailure(mod);
continue;
}
try {
if (log.isDebugEnabled()) {
log.debug("starting module: " + mod.getModuleId());
}
startModule(mod);
}
catch (Exception e) {
log.error("Error while starting module: " + mod.getName(), e);
mod.setStartupErrorMessage("Error while starting module", e);
notifySuperUsersAboutModuleFailure(mod);
}
}
}
}
/**
* Obtain the list of modules that should be started
*
* @return list of modules
*/
private static List<Module> getModulesThatShouldStart() {
List<Module> modules = new ArrayList<Module>();
AdministrationService adminService = Context.getAdministrationService();
for (Module mod : getLoadedModulesCoreFirst()) {
String key = mod.getModuleId() + ".started";
String startedProp = adminService.getGlobalProperty(key, null);
String mandatoryProp = adminService.getGlobalProperty(mod.getModuleId() + ".mandatory", null);
boolean isCoreToOpenmrs = mod.isCore() && !ModuleUtil.ignoreCoreModules();
// if a 'moduleid.started' property doesn't exist, start the module anyway
// as this is probably the first time they are loading it
if (startedProp == null || "true".equals(startedProp) || "true".equalsIgnoreCase(mandatoryProp)
|| mod.isMandatory() || isCoreToOpenmrs) {
modules.add(mod);
}
}
return modules;
}
/**
* Sort modules in startup order based on required and aware-of dependencies
*
* @param modules list of modules to sort
* @return list of modules sorted by dependencies
* @throws CycleException
*/
public static List<Module> getModulesInStartupOrder(Collection<Module> modules) throws CycleException {
Graph<Module> graph = new Graph<Module>();
for (Module mod : modules) {
Module toNode = mod;
graph.addNode(toNode);
// Required dependencies
for (String key : mod.getRequiredModules()) {
Module module = getModuleByPackage(key);
Module fromNode = graph.getNode(module);
if (fromNode == null) {
fromNode = module;
}
if (fromNode != null) {
graph.addEdge(graph.new Edge(
fromNode, toNode));
}
}
// Aware-of dependencies
for (String key : mod.getAwareOfModules()) {
Module module = getModuleByPackage(key);
Module fromNode = graph.getNode(module);
if (fromNode == null) {
fromNode = module;
}
if (fromNode != null) {
graph.addEdge(graph.new Edge(
fromNode, toNode));
}
}
}
return graph.topologicalSort();
}
/**
* Send an Alert to all super users that the given module did not start successfully.
*
* @param mod The Module that failed
*/
private static void notifySuperUsersAboutModuleFailure(Module mod) {
try {
// Add the privileges necessary for notifySuperUsers
Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
// Send an alert to all administrators
Context.getAlertService().notifySuperUsers("Module.startupError.notification.message", null, mod.getName());
}
catch (Exception e) {
log.error("Unable to send an alert to the super users", e);
}
finally {
// Remove added privileges
Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
}
}
/**
* Send an Alert to all super users that modules did not start due to cyclic dependencies
*/
private static void notifySuperUsersAboutCyclicDependencies(Exception ex) {
try {
Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
Context.getAlertService().notifySuperUsers("Module.error.cyclicDependencies", ex, ex.getMessage());
}
catch (Exception e) {
log.error("Unable to send an alert to the super users", e);
}
finally {
Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
}
}
/**
* Returns all modules found/loaded into the system (started and not started), with the core
* modules at the start of that list
*
* @return <code>List<Module></code> of the modules loaded into the system, with the core
* modules first.
*/
public static List<Module> getLoadedModulesCoreFirst() {
List<Module> list = new ArrayList<Module>(getLoadedModules());
final Collection<String> coreModuleIds = ModuleConstants.CORE_MODULES.keySet();
Collections.sort(list, new Comparator<Module>() {
@Override
public int compare(Module left, Module right) {
Integer leftVal = coreModuleIds.contains(left.getModuleId()) ? 0 : 1;
Integer rightVal = coreModuleIds.contains(right.getModuleId()) ? 0 : 1;
return leftVal.compareTo(rightVal);
}
});
return list;
}
/**
* Convenience method to return a List of Strings containing a description of which modules the
* passed module requires but which are not started. The returned description of each module is
* the moduleId followed by the required version if one is specified
*
* @param module the module to check required modules for
* @return List<String> of module names + optional required versions:
* "org.openmrs.formentry 1.8, org.rg.patientmatching"
*/
private static List<String> getMissingRequiredModules(Module module) {
List<String> ret = new ArrayList<String>();
for (String moduleName : module.getRequiredModules()) {
boolean started = false;
for (Module mod : getStartedModules()) {
if (mod.getPackageName().equals(moduleName)) {
String reqVersion = module.getRequiredModuleVersion(moduleName);
if (reqVersion == null || ModuleUtil.compareVersion(mod.getVersion(), reqVersion) >= 0) {
started = true;
}
break;
}
}
if (!started) {
String moduleVersion = module.getRequiredModuleVersion(moduleName);
moduleName = moduleName.replace("org.openmrs.module.", "").replace("org.openmrs.", "");
ret.add(moduleName + (moduleVersion != null ? " " + moduleVersion : ""));
}
}
return ret;
}
/**
* Returns all modules found/loaded into the system (started and not started)
*
* @return <code>Collection<Module></code> of the modules loaded into the system
*/
public static Collection<Module> getLoadedModules() {
if (getLoadedModulesMap().size() > 0) {
return getLoadedModulesMap().values();
}
return Collections.emptyList();
}
/**
* Returns all modules found/loaded into the system (started and not started) in the form of a
* map<ModuleId, Module>
*
* @return map<ModuleId, Module>
*/
public static Map<String, Module> getLoadedModulesMap() {
if (loadedModules == null) {
loadedModules = new WeakHashMap<String, Module>();
}
return loadedModules;
}
/**
* Returns all modules found/loaded into the system (started and not started) in the form of a
* map<PackageName, Module>
*
* @return map<PackageName, Module>
*/
public static Map<String, Module> getLoadedModulesMapPackage() {
if (loadedModules == null) {
loadedModules = new WeakHashMap<String, Module>();
return loadedModules;
}
Map<String, Module> map = new WeakHashMap<String, Module>();
for (String key : loadedModules.keySet()) {
map.put(loadedModules.get(key).getPackageName(), loadedModules.get(key));
}
return map;
}
/**
* Returns the modules that have been successfully started
*
* @return <code>Collection<Module></code> of the started modules
*/
public static Collection<Module> getStartedModules() {
if (getStartedModulesMap().size() > 0) {
return getStartedModulesMap().values();
}
return Collections.emptyList();
}
public static List<Module> getStartedModulesInOrder() {
List<Module> modules = new ArrayList<Module>();
if (actualStartupOrder != null) {
for (String moduleId : actualStartupOrder) {
modules.add(getStartedModulesMap().get(moduleId));
}
} else {
modules.addAll(getStartedModules());
}
return modules;
}
/**
* Returns the modules that have been successfully started in the form of a map<ModuleId,
* Module>
*
* @return Map<ModuleId, Module>
*/
public static Map<String, Module> getStartedModulesMap() {
if (startedModules == null) {
startedModules = new WeakHashMap<String, Module>();
}
return startedModules;
}
/**
* Creates a Module object from the (jar)file pointed to by <code>moduleFile</code> returns null
* if an error occurred during processing
*
* @param moduleFile
* @return module Module
*/
private static Module getModuleFromFile(File moduleFile) throws ModuleException {
Module module = null;
try {
module = new ModuleFileParser(moduleFile).parse();
}
catch (ModuleException e) {
if (moduleFile != null) {
log.error("Error getting module object from file " + moduleFile.getName(), e);
} else {
log.error("Module was null.", e);
}
throw e;
}
return module;
}
/**
* @param moduleId
* @return Module matching module id or null if none
*/
public static Module getModuleById(String moduleId) {
return getLoadedModulesMap().get(moduleId);
}
/**
* @param moduleId
* @return Module matching moduleId, if it is started or null otherwise
*/
public static Module getStartedModuleById(String moduleId) {
return getStartedModulesMap().get(moduleId);
}
/**
* @param modulePackage
* @return Module matching module package or null if none
*/
public static Module getModuleByPackage(String modulePackage) {
for (Module mod : getLoadedModulesMap().values()) {
if (mod.getPackageName().equals(modulePackage)) {
return mod;
}
}
return null;
}
/**
* @see #startModule(Module, boolean, AbstractRefreshableApplicationContext)
* @see #startModuleInternal(Module)
* @see Daemon#startModule(Module)
*/
public static Module startModule(Module module) throws ModuleException {
return startModule(module, false, null);
}
/**
* Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
* Module's activator. This method is run in a new thread and is authenticated as the Daemon
* user. If a non null application context is passed in, it gets refreshed to make the module's
* services available
*
* @param module Module to start
* @param isOpenmrsStartup Specifies whether this module is being started at application startup
* or not, this argument is ignored if a null application context is passed in
* @param applicationContext the spring application context instance to refresh
* @throws ModuleException if the module throws any kind of error at startup or in an activator
* @see #startModuleInternal(Module, boolean, AbstractRefreshableApplicationContext)
* @see Daemon#startModule(Module, boolean, AbstractRefreshableApplicationContext)
*/
public static Module startModule(Module module, boolean isOpenmrsStartup,
AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
if (!requiredModulesStarted(module)) {
int missingModules = 0;
for (String packageName : module.getRequiredModulesMap().keySet()) {
Module mod = getModuleByPackage(packageName);
// mod not installed
if (mod == null) {
missingModules++;
continue;
}
if (!mod.isStarted()) {
startModule(mod);
}
}
if (missingModules > 0) {
String message = getFailedToStartModuleMessage(module);
log.error(message);
module.setStartupErrorMessage(message);
notifySuperUsersAboutModuleFailure(module);
// instead of return null, i realized that Daemon.startModule() always returns a Module
// object,irrespective of whether the startup succeeded
return module;
}
}
return Daemon.startModule(module, isOpenmrsStartup, applicationContext);
}
/**
* This method should not be called directly.<br>
* <br>
* The {@link #startModule(Module)} (and hence {@link Daemon#startModule(Module)}) calls this
* method in a new Thread and is authenticated as the {@link Daemon} user<br>
* <br>
* Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
* Module's activator.
*
* @param module Module to start
*/
public static Module startModuleInternal(Module module) throws ModuleException {
return startModuleInternal(module, false, null);
}
/**
* This method should not be called directly.<br>
* <br>
* The {@link #startModule(Module)} (and hence {@link Daemon#startModule(Module)}) calls this
* method in a new Thread and is authenticated as the {@link Daemon} user<br>
* <br>
* Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
* Module's activator. <br>
* <br>
* If a non null application context is passed in, it gets refreshed to make the module's
* services available
*
* @param module Module to start
* @param isOpenmrsStartup Specifies whether this module is being started at application startup
* or not, this argument is ignored if a null application context is passed in
* @param applicationContext the spring application context instance to refresh
*/
public static Module startModuleInternal(Module module, boolean isOpenmrsStartup,
AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
if (module != null) {
String moduleId = module.getModuleId();
try {
// check to be sure this module can run with our current version
// of OpenMRS code
String requireVersion = module.getRequireOpenmrsVersion();
ModuleUtil.checkRequiredVersion(OpenmrsConstants.OPENMRS_VERSION_SHORT, requireVersion);
// check for required modules
if (!requiredModulesStarted(module)) {
throw new ModuleException(getFailedToStartModuleMessage(module));
}
// fire up the classloader for this module
ModuleClassLoader moduleClassLoader = new ModuleClassLoader(module, ModuleFactory.class.getClassLoader());
getModuleClassLoaderMap().put(module, moduleClassLoader);
registerProvidedPackages(moduleClassLoader);
// don't load the advice objects into the Context
// At startup, the spring context isn't refreshed until all modules
// have been loaded. This causes errors if called here during a
// module's startup if one of these advice points is on another
// module because that other module's service won't have been loaded
// into spring yet. All advice for all modules must be reloaded
// a spring context refresh anyway, so skip the advice loading here
// loadAdvice(module);
// map extension point to a list of extensions for this module only
Map<String, List<Extension>> moduleExtensionMap = new HashMap<String, List<Extension>>();
for (Extension ext : module.getExtensions()) {
String extId = ext.getExtensionId();
List<Extension> tmpExtensions = moduleExtensionMap.get(extId);
if (tmpExtensions == null) {
tmpExtensions = new Vector<Extension>();
moduleExtensionMap.put(extId, tmpExtensions);
}
tmpExtensions.add(ext);
}
// Sort this module's extensions, and merge them into the full extensions map
Comparator<Extension> sortOrder = new Comparator<Extension>() {
@Override
public int compare(Extension e1, Extension e2) {
return Integer.valueOf(e1.getOrder()).compareTo(Integer.valueOf(e2.getOrder()));
}
};
for (Map.Entry<String, List<Extension>> moduleExtensionEntry : moduleExtensionMap.entrySet()) {
// Sort this module's extensions for current extension point
List<Extension> sortedModuleExtensions = moduleExtensionEntry.getValue();
Collections.sort(sortedModuleExtensions, sortOrder);
// Get existing extensions, and append the ones from the new module
List<Extension> extensions = getExtensionMap().get(moduleExtensionEntry.getKey());
if (extensions == null) {
extensions = new Vector<Extension>();
getExtensionMap().put(moduleExtensionEntry.getKey(), extensions);
}
for (Extension ext : sortedModuleExtensions) {
log.debug("Adding to mapping ext: " + ext.getExtensionId() + " ext.class: " + ext.getClass());
extensions.add(ext);
}
}
// run the module's sql update script
// This and the property updates are the only things that can't
// be undone at startup, so put these calls after any other
// calls that might hinder startup
SortedMap<String, String> diffs = SqlDiffFileParser.getSqlDiffs(module);
try {
// this method must check and run queries against the database.
// to do this, it must be "authenticated". Give the current
// "user" the proxy privilege so this can be done. ("user" might
// be nobody because this is being run at startup)
Context.addProxyPrivilege("");
for (Map.Entry<String, String> entry : diffs.entrySet()) {
String version = entry.getKey();
String sql = entry.getValue();
if (StringUtils.hasText(sql)) {
runDiff(module, version, sql);
}
}
}
finally {
// take the "authenticated" privilege away from the current "user"
Context.removeProxyPrivilege("");
}
// run module's optional liquibase.xml immediately after sqldiff.xml
runLiquibase(module);
// effectively mark this module as started successfully
getStartedModulesMap().put(moduleId, module);
if (actualStartupOrder == null) {
actualStartupOrder = new LinkedHashSet<String>();
}
actualStartupOrder.add(moduleId);
try {
// save the state of this module for future restarts
saveGlobalProperty(moduleId + ".started", "true", getGlobalPropertyStartedDescription(moduleId));
// save the mandatory status
saveGlobalProperty(moduleId + ".mandatory", String.valueOf(module.isMandatory()),
getGlobalPropertyMandatoryModuleDescription(moduleId));
}
catch (Exception e) {
// pass over errors because this doesn't really concern startup
// passing over this also allows for multiple of the same-named modules
// to be loaded in junit tests that are run within one session
log.debug("Got an error when trying to set the global property on module startup", e);
}
// (this must be done after putting the module in the started
// list)
// if this module defined any privileges or global properties,
// make sure they are added to the database
// (Unfortunately, placing the call here will duplicate work
// done at initial app startup)
if (!module.getPrivileges().isEmpty() || !module.getGlobalProperties().isEmpty()) {
log.debug("Updating core dataset");
Context.checkCoreDataset();
// checkCoreDataset() currently doesn't throw an error. If
// it did, it needs to be
// caught and the module needs to be stopped and given a
// startup error
}
// should be near the bottom so the module has all of its stuff
// set up for it already.
try {
if (module.getModuleActivator() != null) {
// if extends BaseModuleActivator
module.getModuleActivator().willStart();
}
}
catch (ModuleException e) {
// just rethrow module exceptions. This should be used for a
// module marking that it had trouble starting
throw e;
}
catch (Exception e) {
throw new ModuleException("Error while calling module's Activator.startup()/willStart() method", e);
}
// erase any previous startup error
module.clearStartupError();
}
catch (Exception e) {
log.warn("Error while trying to start module: " + moduleId, e);
module.setStartupErrorMessage("Error while trying to start module", e);
notifySuperUsersAboutModuleFailure(module);
// undo all of the actions in startup
try {
boolean skipOverStartedProperty = false;
if (e instanceof ModuleMustStartException) {
skipOverStartedProperty = true;
}
stopModule(module, skipOverStartedProperty, true);
}
catch (Exception e2) {
// this will probably occur about the same place as the
// error in startup
log.debug("Error while stopping module: " + moduleId, e2);
}
}
}
if (applicationContext != null) {
ModuleUtil.refreshApplicationContext(applicationContext, isOpenmrsStartup, module);
}
return module;
}
private static void registerProvidedPackages(ModuleClassLoader moduleClassLoader) {
for (String providedPackage : moduleClassLoader.getProvidedPackages()) {
Set<ModuleClassLoader> newSet = new HashSet<ModuleClassLoader>();
Set<ModuleClassLoader> set = providedPackages.get(providedPackage);
if (set != null) {
newSet.addAll(set);
}
newSet.add(moduleClassLoader);
providedPackages.put(providedPackage, newSet);
}
}
private static void unregisterProvidedPackages(ModuleClassLoader moduleClassLoader) {
for (String providedPackage : moduleClassLoader.getProvidedPackages()) {
Set<ModuleClassLoader> newSet = new HashSet<ModuleClassLoader>();
Set<ModuleClassLoader> set = providedPackages.get(providedPackage);
if (set != null) {
newSet.addAll(set);
}
newSet.remove(moduleClassLoader);
providedPackages.put(providedPackage, newSet);
}
}
public static Set<ModuleClassLoader> getModuleClassLoadersForPackage(String packageName) {
Set<ModuleClassLoader> set = providedPackages.get(packageName);
if (set == null) {
return Collections.emptySet();
} else {
return new HashSet<ModuleClassLoader>(set);
}
}
/**
* Gets the error message of a module which fails to start.
*
* @param module the module that has failed to start.
* @return the message text.
*/
private static String getFailedToStartModuleMessage(Module module) {
String[] params = { module.getName(), OpenmrsUtil.join(getMissingRequiredModules(module), ", ") };
return Context.getMessageSourceService().getMessage("Module.error.moduleCannotBeStarted", params,
Context.getLocale());
}
/**
* Gets the error message of cyclic dependencies between modules
*
* @return the message text.
*/
private static String getCyclicDependenciesMessage(String message) {
return Context.getMessageSourceService().getMessage("Module.error.cyclicDependencies", new Object[]{ message }, Context.getLocale());
}
/**
* Loop over the given module's advice objects and load them into the Context This needs to be
* called for all started modules after every restart of the Spring Application Context
*
* @param module
*/
public static void loadAdvice(Module module) {
for (AdvicePoint advice : module.getAdvicePoints()) {
Class<?> cls = null;
try {
cls = Context.loadClass(advice.getPoint());
Object aopObject = advice.getClassInstance();
if (Advisor.class.isInstance(aopObject)) {
log.debug("adding advisor: " + aopObject.getClass());
Context.addAdvisor(cls, (Advisor) aopObject);
} else {
log.debug("Adding advice: " + aopObject.getClass());
Context.addAdvice(cls, (Advice) aopObject);
}
}
catch (ClassNotFoundException e) {
log.warn("Could not load advice point: " + advice.getPoint(), e);
}
}
}
/**
* Execute the given sql diff section for the given module
*
* @param module the module being executed on
* @param version the version of this sql diff
* @param sql the actual sql statements to run (separated by semi colons)
*/
private static void runDiff(Module module, String version, String sql) {
AdministrationService as = Context.getAdministrationService();
String key = module.getModuleId() + ".database_version";
GlobalProperty gp = as.getGlobalPropertyObject(key);
boolean executeSQL = false;
// check given version against current version
if (gp != null && StringUtils.hasLength(gp.getPropertyValue())) {
String currentDbVersion = gp.getPropertyValue();
if (log.isDebugEnabled()) {
log.debug("version:column " + version + ":" + currentDbVersion);
log.debug("compare: " + ModuleUtil.compareVersion(version, currentDbVersion));
}
if (ModuleUtil.compareVersion(version, currentDbVersion) > 0) {
executeSQL = true;
}
} else {
executeSQL = true;
}
// version is greater than the currently installed version. execute this update.
if (executeSQL) {
try {
Context.addProxyPrivilege(PrivilegeConstants.SQL_LEVEL_ACCESS);
log.debug("Executing sql: " + sql);
String[] sqlStatements = sql.split(";");
for (String sqlStatement : sqlStatements) {
if (sqlStatement.trim().length() > 0) {
as.executeSQL(sqlStatement, false);
}
}
}
finally {
Context.removeProxyPrivilege(PrivilegeConstants.SQL_LEVEL_ACCESS);
}
// save the global property
try {
Context.addProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);
String description = "DO NOT MODIFY. Current database version number for the " + module.getModuleId()
+ " module.";
if (gp == null) {
log.info("Global property " + key + " was not found. Creating one now.");
gp = new GlobalProperty(key, version, description);
as.saveGlobalProperty(gp);
} else if (!gp.getPropertyValue().equals(version)) {
log.info("Updating global property " + key + " to version: " + version);
gp.setDescription(description);
gp.setPropertyValue(version);