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

openmrs / openmrs-core / 21728229743

05 Feb 2026 08:58PM UTC coverage: 63.392% (+0.08%) from 63.315%
21728229743

push

github

web-flow
TRUNK-6534: Support for creation linked orders (#5736) (#5741)

* TRUNK-6534 | Add. Support for creating linked orders

- Fixes the OrderService to not validate same orderable concept for new orders linked with previous orders.

* TRUNK-6534 | Refactor. Simplify assertion

1 of 1 new or added line in 1 file covered. (100.0%)

127 existing lines in 6 files now uncovered.

23116 of 36465 relevant lines covered (63.39%)

0.63 hits per line

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

51.65
/api/src/main/java/org/openmrs/module/ModuleUtil.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.ByteArrayInputStream;
13
import java.io.ByteArrayOutputStream;
14
import java.io.File;
15
import java.io.FileOutputStream;
16
import java.io.IOException;
17
import java.io.InputStream;
18
import java.net.HttpURLConnection;
19
import java.net.MalformedURLException;
20
import java.net.URL;
21
import java.net.URLConnection;
22
import java.nio.charset.StandardCharsets;
23
import java.util.ArrayList;
24
import java.util.Collection;
25
import java.util.Collections;
26
import java.util.Enumeration;
27
import java.util.HashSet;
28
import java.util.LinkedHashSet;
29
import java.util.List;
30
import java.util.Properties;
31
import java.util.Set;
32
import java.util.jar.JarEntry;
33
import java.util.jar.JarFile;
34
import java.util.regex.Matcher;
35
import java.util.regex.Pattern;
36
import java.util.zip.ZipEntry;
37
import org.openmrs.api.OpenmrsService;
38
import org.springframework.beans.PropertyValue;
39
import org.springframework.beans.factory.annotation.Autowired;
40
import org.springframework.beans.factory.config.BeanDefinition;
41
import org.springframework.beans.factory.config.BeanDefinitionHolder;
42
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
43
import org.springframework.beans.factory.config.RuntimeBeanReference;
44
import org.springframework.context.ConfigurableApplicationContext;
45
import org.springframework.core.annotation.AnnotationUtils;
46

47
import java.lang.reflect.Constructor;
48
import java.lang.reflect.Field;
49
import java.lang.reflect.Method;
50
import org.apache.commons.io.IOUtils;
51
import org.apache.commons.lang3.StringUtils;
52
import org.apache.commons.lang3.math.NumberUtils;
53
import org.openmrs.GlobalProperty;
54
import org.openmrs.api.AdministrationService;
55
import org.openmrs.api.context.Context;
56
import org.openmrs.api.context.ServiceContext;
57
import org.openmrs.scheduler.SchedulerUtil;
58
import org.openmrs.util.OpenmrsClassLoader;
59
import org.openmrs.util.OpenmrsConstants;
60
import org.openmrs.util.OpenmrsUtil;
61
import org.slf4j.Logger;
62
import org.slf4j.LoggerFactory;
63
import org.springframework.context.support.AbstractRefreshableApplicationContext;
64

65
/**
66
 * Utility methods for working and manipulating modules
67
 */
68
public class ModuleUtil {
69

70
        private ModuleUtil() {
71
        }
72
        
73
        private static final Logger log = LoggerFactory.getLogger(ModuleUtil.class);
1✔
74
        
75
        /**
76
         * Start up the module system with the given properties.
77
         *
78
         * @param props Properties (OpenMRS runtime properties)
79
         */
80
        public static void startup(Properties props) throws ModuleMustStartException {
81
                
82
                String moduleListString = props.getProperty(ModuleConstants.RUNTIMEPROPERTY_MODULE_LIST_TO_LOAD);
1✔
83
                
84
                if (moduleListString == null || moduleListString.isEmpty()) {
1✔
85
                        // Attempt to get all of the modules from the modules folder
86
                        // and store them in the modules list
87
                        log.debug("Starting all modules");
×
88
                        ModuleFactory.loadModules();
×
89
                } else {
90
                        // use the list of modules and load only those
91
                        log.debug("Starting all modules in this list: " + moduleListString);
1✔
92
                        
93
                        String[] moduleArray = moduleListString.split(" ");
1✔
94
                        List<File> modulesToLoad = new ArrayList<>();
1✔
95
                        
96
                        for (String modulePath : moduleArray) {
1✔
97
                                if (modulePath != null && modulePath.length() > 0) {
1✔
98
                                        File file = new File(modulePath);
1✔
99
                                        if (file.exists()) {
1✔
100
                                                modulesToLoad.add(file);
×
101
                                        } else {
102
                                                // try to load the file from the classpath
103
                                                InputStream stream = ModuleUtil.class.getClassLoader().getResourceAsStream(modulePath);
1✔
104
                                                
105
                                                // expand the classpath-found file to a temporary location
106
                                                if (stream != null) {
1✔
107
                                                        try {
108
                                                                // get and make a temp directory if necessary
109
                                                                String tmpDir = System.getProperty("java.io.tmpdir");
1✔
110
                                                                File expandedFile = File.createTempFile(file.getName() + "-", ".omod", new File(tmpDir));
1✔
111
                                                                
112
                                                                // pull the name from the absolute path load attempt
113
                                                                FileOutputStream outStream = new FileOutputStream(expandedFile, false);
1✔
114
                                                                
115
                                                                // do the actual file copying
116
                                                                OpenmrsUtil.copyFile(stream, outStream);
1✔
117
                                                                
118
                                                                // add the freshly expanded file to the list of modules we're going to start up
119
                                                                modulesToLoad.add(expandedFile);
1✔
120
                                                                expandedFile.deleteOnExit();
1✔
121
                                                        }
122
                                                        catch (IOException io) {
×
123
                                                                log.error("Unable to expand classpath found module: " + modulePath, io);
×
124
                                                        }
1✔
125
                                                } else {
126
                                                        log
×
127
                                                                .error("Unable to load module at path: "
×
128
                                                                        + modulePath
129
                                                                        + " because no file exists there and it is not found on the classpath. (absolute path tried: "
130
                                                                        + file.getAbsolutePath() + ")");
×
131
                                                }
132
                                        }
133
                                }
134
                        }
135
                        
136
                        ModuleFactory.loadModules(modulesToLoad);
1✔
137
                }
138
                
139
                // start all of the modules we just loaded
140
                ModuleFactory.startModules();
1✔
141
                
142
                // some debugging info
143
                if (log.isDebugEnabled()) {
1✔
144
                        Collection<Module> modules = ModuleFactory.getStartedModules();
×
145
                        if (modules == null || modules.isEmpty()) {
×
146
                                log.debug("No modules loaded");
×
147
                        } else {
148
                                log.debug("Found and loaded {} module(s)", modules.size());
×
149
                        }
150
                }
151
                
152
                // make sure all mandatory modules are loaded and started
153
                checkMandatoryModulesStarted();
1✔
154
        }
1✔
155
        
156
        /**
157
         * Stops the module system by calling stopModule for all modules that are currently started
158
         */
159
        public static void shutdown() {
160

161
                List<Module> modules = new ArrayList<>(ModuleFactory.getStartedModules());
1✔
162
                
163
                for (Module mod : modules) {
1✔
164
                        log.debug("stopping module: {}", mod.getModuleId());
1✔
165
                        
166
                        if (mod.isStarted()) {
1✔
167
                                ModuleFactory.stopModule(mod, true, true);
1✔
168
                        }
169
                }
1✔
170
                
171
                log.debug("done shutting down modules");
1✔
172
                
173
                // clean up the static variables just in case they weren't done before
174
                ModuleFactory.extensionMap.clear();
1✔
175
                ModuleFactory.loadedModules.invalidateAll();
1✔
176
                ModuleFactory.moduleClassLoaders.invalidateAll();
1✔
177
                ModuleFactory.startedModules.invalidateAll();
1✔
178
        }
1✔
179
        
180
        /**
181
         * Add the <code>inputStream</code> as a file in the modules repository
182
         *
183
         * @param inputStream <code>InputStream</code> to load
184
         * @return filename String of the file's name of the stream
185
         */
186
        public static File insertModuleFile(InputStream inputStream, String filename) {
187
                File folder = getModuleRepository();
×
188
                
189
                // check if module filename is already loaded
190
                if (OpenmrsUtil.folderContains(folder, filename)) {
×
191
                        throw new ModuleException(filename + " is already associated with a loaded module.");
×
192
                }
193
                
194
                File file = new File(folder.getAbsolutePath(), filename);
×
195
                
196
                try (FileOutputStream outputStream = new FileOutputStream(file)) {
×
197
                        OpenmrsUtil.copyFile(inputStream, outputStream);
×
198
                }
199
                catch (IOException e) {
×
200
                        throw new ModuleException("Can't create module file for " + filename, e);
×
201
                }
202
                finally {
203
                        try {
204
                                inputStream.close();
×
205
                        }
206
                        catch (Exception e) { /* pass */}
×
207
                }
208
                
209
                return file;
×
210
        }
211

212
        /**
213
         * Checks if the current OpenMRS version is in an array of versions.
214
         * <p>
215
         * This method calls {@link ModuleUtil#matchRequiredVersions(String, String)} internally.
216
         * </p>
217
         *
218
         * @param versions the openmrs versions to be checked against the current openmrs version
219
         * @return true if the current openmrs version is in versions otherwise false
220
         * <strong>Should</strong> return false when versions is null
221
         * <strong>Should</strong> return false when versions is empty
222
         * <strong>Should</strong> return true if current openmrs version matches one element in versions
223
         * <strong>Should</strong> return false if current openmrs version does not match any element in versions
224
         */
225
        public static boolean isOpenmrsVersionInVersions(String ...versions) {
226
                return isVersionInVersions(OpenmrsConstants.OPENMRS_VERSION_SHORT, versions);
1✔
227
        }
228

229
        /**
230
         * For testing of {@link #isOpenmrsVersionInVersions(String...)} only.
231
         * 
232
         * @param version the version
233
         * @param versions versions to match
234
         * @return true if version matches any value from versions
235
         */
236
        static boolean isVersionInVersions(String version, String ...versions) {
237
                if (versions == null || versions.length == 0) {
1✔
238
                        return false;
1✔
239
                }
240

241
                boolean result = false;
1✔
242
                for (String candidateVersion : versions) {
1✔
243
                        if (matchRequiredVersions(version, candidateVersion)) {
1✔
244
                                result = true;
1✔
245
                                break;
1✔
246
                        }
247
                }
248
                return result;
1✔
249
        }
250
        
251
        /**
252
         * This method is an enhancement of {@link #compareVersion(String, String)} and adds support for
253
         * wildcard characters and upperbounds. <br>
254
         * <br>
255
         * This method calls {@link ModuleUtil#checkRequiredVersion(String, String)} internally. <br>
256
         * <br>
257
         * The require version number in the config file can be in the following format:
258
         * <ul>
259
         * <li>1.2.3</li>
260
         * <li>1.2.*</li>
261
         * <li>1.2.2 - 1.2.3</li>
262
         * <li>1.2.* - 1.3.*</li>
263
         * </ul>
264
         * <p>
265
         * Again the possible require version number formats with their interpretation:
266
         * <ul>
267
         * <li>1.2.3 means 1.2.3 and above</li>
268
         * <li>1.2.* means any version of the 1.2.x branch. That is 1.2.0, 1.2.1, 1.2.2,... but not 1.3.0, 1.4.0</li>
269
         * <li>1.2.2 - 1.2.3 means 1.2.2 and 1.2.3 (inclusive)</li>
270
         * <li>1.2.* - 1.3.* means any version of the 1.2.x and 1.3.x branch</li>
271
         * </ul>
272
         * </p>
273
         *
274
         * @param version openmrs version number to be compared
275
         * @param versionRange value in the config file for required openmrs version
276
         * @return true if the <code>version</code> is within the <code>value</code>
277
         * <strong>Should</strong> allow ranged required version
278
         * <strong>Should</strong> allow ranged required version with wild card
279
         * <strong>Should</strong> allow ranged required version with wild card on one end
280
         * <strong>Should</strong> allow single entry for required version
281
         * <strong>Should</strong> allow required version with wild card
282
         * <strong>Should</strong> allow non numeric character required version
283
         * <strong>Should</strong> allow ranged non numeric character required version
284
         * <strong>Should</strong> allow ranged non numeric character with wild card
285
         * <strong>Should</strong> allow ranged non numeric character with wild card on one end
286
         * <strong>Should</strong> return false when openmrs version beyond wild card range
287
         * <strong>Should</strong> return false when required version beyond openmrs version
288
         * <strong>Should</strong> return false when required version with wild card beyond openmrs version
289
         * <strong>Should</strong> return false when required version with wild card on one end beyond openmrs version
290
         * <strong>Should</strong> return false when single entry required version beyond openmrs version
291
         * <strong>Should</strong> allow release type in the version
292
         * <strong>Should</strong> match when revision number is below maximum revision number
293
         * <strong>Should</strong> not match when revision number is above maximum revision number
294
         * <strong>Should</strong> correctly set upper and lower limit for versionRange with qualifiers and wild card
295
         * <strong>Should</strong> match when version has wild card plus qualifier and is within boundary
296
         * <strong>Should</strong> not match when version has wild card plus qualifier and is outside boundary
297
         * <strong>Should</strong> match when version has wild card and is within boundary
298
         * <strong>Should</strong> not match when version has wild card and is outside boundary
299
         * <strong>Should</strong> return true when required version is empty
300
         */
301
        public static boolean matchRequiredVersions(String version, String versionRange) {
302
                // There is a null check so no risk in keeping the literal on the right side
303
                if (StringUtils.isNotEmpty(versionRange)) {
1✔
304
                        String[] ranges = versionRange.split(",");
1✔
305
                        for (String range : ranges) {
1✔
306
                                // need to externalize this string
307
                                String separator = "-";
1✔
308
                                if (range.indexOf("*") > 0 || range.indexOf(separator) > 0 && (!isVersionWithQualifier(range))) {
1✔
309
                                        // if it contains "*" or "-" then we must separate those two
310
                                        // assume it's always going to be two part
311
                                        // assign the upper and lower bound
312
                                        // if there's no "-" to split lower and upper bound
313
                                        // then assign the same value for the lower and upper
314
                                        String lowerBound = range;
1✔
315
                                        String upperBound = range;
1✔
316
                                        
317
                                        int indexOfSeparator = range.indexOf(separator);
1✔
318
                                        while (indexOfSeparator > 0) {
1✔
319
                                                lowerBound = range.substring(0, indexOfSeparator);
1✔
320
                                                upperBound = range.substring(indexOfSeparator + 1);
1✔
321
                                                if (upperBound.matches("^\\s?\\d+.*")) {
1✔
322
                                                        break;
1✔
323
                                                }
324
                                                indexOfSeparator = range.indexOf(separator, indexOfSeparator + 1);
1✔
325
                                        }
326
                                        
327
                                        // only preserve part of the string that match the following format:
328
                                        // - xx.yy.*
329
                                        // - xx.yy.zz*
330
                                        lowerBound = StringUtils.remove(lowerBound, lowerBound.replaceAll("^\\s?\\d+[\\.\\d+\\*?|\\.\\*]+", ""));
1✔
331
                                        upperBound = StringUtils.remove(upperBound, upperBound.replaceAll("^\\s?\\d+[\\.\\d+\\*?|\\.\\*]+", ""));
1✔
332
                                        
333
                                        // if the lower contains "*" then change it to zero
334
                                        if (lowerBound.indexOf("*") > 0) {
1✔
335
                                                lowerBound = lowerBound.replaceAll("\\*", "0");
1✔
336
                                        }
337
                                        
338
                                        // if the upper contains "*" then change it to maxRevisionNumber
339
                                        if (upperBound.indexOf("*") > 0) {
1✔
340
                                                upperBound = upperBound.replaceAll("\\*", Integer.toString(Integer.MAX_VALUE));
1✔
341
                                        }
342
                                        
343
                                        int lowerReturn = compareVersionIgnoringQualifier(version, lowerBound);
1✔
344
                                        
345
                                        int upperReturn = compareVersionIgnoringQualifier(version, upperBound);
1✔
346
                                        
347
                                        if (lowerReturn < 0 || upperReturn > 0) {
1✔
348
                                                log.debug("Version " + version + " is not between " + lowerBound + " and " + upperBound);
1✔
349
                                        } else {
350
                                                return true;
1✔
351
                                        }
352
                                } else {
1✔
353
                                        if (compareVersionIgnoringQualifier(version, range) < 0) {
1✔
354
                                                log.debug("Version " + version + " is below " + range);
1✔
355
                                        } else {
356
                                                return true;
1✔
357
                                        }
358
                                }
359
                        }
360
                }
1✔
361
                else {
362
                        //no version checking if required version is not specified
363
                        return true;
1✔
364
                }
365
                
366
                return false;
1✔
367
        }
368
        
369
        /**
370
         * This method is an enhancement of {@link #compareVersion(String, String)} and adds support for
371
         * wildcard characters and upperbounds. <br>
372
         * <br>
373
         * <br>
374
         * The require version number in the config file can be in the following format:
375
         * <ul>
376
         * <li>1.2.3</li>
377
         * <li>1.2.*</li>
378
         * <li>1.2.2 - 1.2.3</li>
379
         * <li>1.2.* - 1.3.*</li>
380
         * </ul>
381
         * <p>
382
         * Again the possible require version number formats with their interpretation:
383
         * <ul>
384
         * <li>1.2.3 means 1.2.3 and above</li>
385
         * <li>1.2.* means any version of the 1.2.x branch. That is 1.2.0, 1.2.1, 1.2.2,... but not 1.3.0, 1.4.0</li>
386
         * <li>1.2.2 - 1.2.3 means 1.2.2 and 1.2.3 (inclusive)</li>
387
         * <li>1.2.* - 1.3.* means any version of the 1.2.x and 1.3.x branch</li>
388
         * </ul>
389
         * </p>
390
         *
391
         * @param version openmrs version number to be compared
392
         * @param versionRange value in the config file for required openmrs version
393
         * @throws ModuleException if the <code>version</code> is not within the <code>value</code>
394
         * <strong>Should</strong> throw ModuleException if openmrs version beyond wild card range
395
         * <strong>Should</strong> throw ModuleException if required version beyond openmrs version
396
         * <strong>Should</strong> throw ModuleException if required version with wild card beyond openmrs version
397
         * <strong>Should</strong> throw ModuleException if required version with wild card on one end beyond openmrs
398
         *         version
399
         * <strong>Should</strong> throw ModuleException if single entry required version beyond openmrs version
400
         * <strong>Should</strong> throw ModuleException if SNAPSHOT not handled correctly
401
         * <strong>Should</strong> handle SNAPSHOT versions
402
         * <strong>Should</strong> handle ALPHA versions
403
         */
404
        public static void checkRequiredVersion(String version, String versionRange) throws ModuleException {
405
                if (!matchRequiredVersions(version, versionRange)) {
1✔
406
                        String ms = Context.getMessageSourceService().getMessage("Module.requireVersion.outOfBounds",
1✔
407
                            new String[] { versionRange, version }, Context.getLocale());
1✔
408
                        throw new ModuleException(ms);
1✔
409
                }
410
        }
1✔
411
        
412
        /**
413
         * Compare two version strings.
414
         *
415
         * @param versionA String like 1.9.2.0, may include a qualifier like "-SNAPSHOT", may be null
416
         * @param versionB String like 1.9.2.0, may include a qualifier like "-SNAPSHOT", may be null
417
         * @return the value <code>0</code> if versions are equal; a value less than <code>0</code> if first version is
418
         *                    before the second one; a value greater than <code>0</code> if first version is after the second one.
419
         *                    If version numbers are equal and only one of them has a qualifier, the version without the qualifier is
420
         *                    considered greater.
421
         */
422
        public static int compareVersion(String versionA, String versionB) {
423
                return compareVersion(versionA, versionB, false);
1✔
424
        }
425

426
        /**
427
         * Compare two version strings. Any version qualifiers are ignored in the comparison.
428
         *
429
         * @param versionA String like 1.9.2.0, may include a qualifier like "-SNAPSHOT", may be null
430
         * @param versionB String like 1.9.2.0, may include a qualifier like "-SNAPSHOT", may be null
431
         * @return the value <code>0</code> if versions are equal; a value less than <code>0</code> if first version is
432
         *                    before the second one; a value greater than <code>0</code> if first version is after the second one.
433
         */
434
        public static int compareVersionIgnoringQualifier(String versionA, String versionB) {
435
                return compareVersion(versionA, versionB, true);
1✔
436
        }
437

438
        private static int compareVersion(String versionA, String versionB, boolean ignoreQualifier) {
439
                try {
440
                        if (versionA == null || versionB == null) {
1✔
441
                                return 0;
1✔
442
                        }
443

444
                        List<String> versionANumbers = new ArrayList<>();
1✔
445
                        List<String> versionBNumbers = new ArrayList<>();
1✔
446
                        String qualifierSeparator = "-";
1✔
447

448
                        // strip off any qualifier e.g. "-SNAPSHOT"
449
                        int qualifierIndexA = versionA.indexOf(qualifierSeparator);
1✔
450
                        if (qualifierIndexA != -1) {
1✔
451
                                versionA = versionA.substring(0, qualifierIndexA);
1✔
452
                        }
453

454
                        // strip off any qualifier e.g. "-SNAPSHOT"
455
                        int qualifierIndexB = versionB.indexOf(qualifierSeparator);
1✔
456
                        if (qualifierIndexB != -1) {
1✔
457
                                versionB = versionB.substring(0, qualifierIndexB);
1✔
458
                        }
459

460
                        Collections.addAll(versionANumbers, versionA.split("\\."));
1✔
461
                        Collections.addAll(versionBNumbers, versionB.split("\\."));
1✔
462

463
                        // match the sizes of the lists
464
                        while (versionANumbers.size() < versionBNumbers.size()) {
1✔
465
                                versionANumbers.add("0");
1✔
466
                        }
467
                        while (versionBNumbers.size() < versionANumbers.size()) {
1✔
468
                                versionBNumbers.add("0");
1✔
469
                        }
470

471
                        for (int x = 0; x < versionANumbers.size(); x++) {
1✔
472
                                String verAPartString = versionANumbers.get(x).trim();
1✔
473
                                String verBPartString = versionBNumbers.get(x).trim();
1✔
474
                                Long verAPart = NumberUtils.toLong(verAPartString, 0);
1✔
475
                                Long verBPart = NumberUtils.toLong(verBPartString, 0);
1✔
476

477
                                int ret = verAPart.compareTo(verBPart);
1✔
478
                                if (ret != 0) {
1✔
479
                                        return ret;
1✔
480
                                }
481
                        }
482
                        
483
                        // At this point the version numbers are equal.
484
                        if (!ignoreQualifier) {
1✔
485
                                if (qualifierIndexA >= 0 && qualifierIndexB < 0) {
1✔
486
                                        return -1;
1✔
487
                                } else if (qualifierIndexA < 0 && qualifierIndexB >= 0) {
1✔
488
                                        return 1;
1✔
489
                                }
490
                        }
491
                }
492
                catch (NumberFormatException e) {
×
493
                        log.error("Error while converting a version/value to an integer: " + versionA + "/" + versionB, e);
×
494
                }
1✔
495
                
496
                // default return value if an error occurs or elements are equal
497
                return 0;
1✔
498
        }
499
        
500
        /**
501
         * Checks for qualifier version (i.e "-SNAPSHOT", "-ALPHA" etc. after maven version conventions)
502
         *
503
         * @param version String like 1.9.2-SNAPSHOT
504
         * @return true if version contains qualifier
505
         */
506
        public static boolean isVersionWithQualifier(String version) {
507
                Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.(\\d+))?(\\-([A-Za-z]+))").matcher(version);
1✔
508
                return matcher.matches();
1✔
509
        }
510
        
511
        /**
512
         * Gets the folder where modules are stored. ModuleExceptions are thrown on errors
513
         *
514
         * @return folder containing modules
515
         * <strong>Should</strong> use the runtime property as the first choice if specified
516
         * <strong>Should</strong> return the correct file if the runtime property is an absolute path
517
         */
518
        public static File getModuleRepository() {
519
                
520
                String folderName = Context.getRuntimeProperties().getProperty(ModuleConstants.REPOSITORY_FOLDER_RUNTIME_PROPERTY);
1✔
521
                if (StringUtils.isBlank(folderName)) {
1✔
522
                        AdministrationService as = Context.getAdministrationService();
1✔
523
                        folderName = as.getGlobalProperty(ModuleConstants.REPOSITORY_FOLDER_PROPERTY,
1✔
524
                            ModuleConstants.REPOSITORY_FOLDER_PROPERTY_DEFAULT);
525
                }
526
                // try to load the repository folder straight away.
527
                File folder = new File(folderName);
1✔
528
                
529
                // if the property wasn't a full path already, assume it was intended to be a folder in the
530
                // application directory
531
                if (!folder.exists()) {
1✔
532
                        folder = new File(OpenmrsUtil.getApplicationDataDirectory(), folderName);
1✔
533
                }
534
                
535
                // now create the modules folder if it doesn't exist
536
                if (!folder.exists()) {
1✔
537
                        log.warn("Module repository " + folder.getAbsolutePath() + " doesn't exist.  Creating directories now.");
1✔
538
                        folder.mkdirs();
1✔
539
                }
540
                
541
                if (!folder.isDirectory()) {
1✔
542
                        throw new ModuleException("Module repository is not a directory at: " + folder.getAbsolutePath());
×
543
                }
544
                
545
                return folder;
1✔
546
        }
547
        
548
        /**
549
         * Utility method to convert a {@link File} object to a local URL.
550
         *
551
         * @param file a file object
552
         * @return absolute URL that points to the given file
553
         * @throws MalformedURLException if file can't be represented as URL for some reason
554
         */
555
        public static URL file2url(final File file) throws MalformedURLException {
556
                if (file == null) {
1✔
557
                        return null;
1✔
558
                }
559
                try {
560
                        return file.getCanonicalFile().toURI().toURL();
1✔
561
                }
562
                catch (IOException | NoSuchMethodError ioe) {
1✔
563
                        throw new MalformedURLException("Cannot convert: " + file.getName() + " to url");
1✔
564
                }
565
        }
566
        
567
        /**
568
         * Expand the given <code>fileToExpand</code> jar to the <code>tmpModuleFile</code> directory
569
         *
570
         * If <code>name</code> is null, the entire jar is expanded. If<code>name</code> is not null,
571
         * then only that path/file is expanded.
572
         *
573
         * @param fileToExpand file pointing at a .jar
574
         * @param tmpModuleDir directory in which to place the files
575
         * @param name filename inside of the jar to look for and expand
576
         * @param keepFullPath if true, will recreate entire directory structure in tmpModuleDir
577
         *            relating to <code>name</code>. if false will start directory structure at
578
         *            <code>name</code>
579
         * @throws UnsupportedOperationException if an entry would be extracted outside of tmpModuleDir (Zip slip attack)
580
         * <strong>Should</strong> expand entire jar if name is null
581
         * <strong>Should</strong> expand entire jar if name is empty string
582
         * <strong>Should</strong> expand directory with parent tree if name is directory and keepFullPath is true
583
         * <strong>Should</strong> expand directory without parent tree if name is directory and keepFullPath is false
584
         * <strong>Should</strong> expand file with parent tree if name is file and keepFullPath is true
585
         * <strong>Should</strong> throw exception for Zip slip attack
586
         */
587
        public static void expandJar(File fileToExpand, File tmpModuleDir, String name, boolean keepFullPath) throws IOException {
588
                String docBase = tmpModuleDir.getAbsolutePath();
1✔
589
                log.debug("Expanding jar {}", fileToExpand);
1✔
590
                try (JarFile jarFile = new JarFile(fileToExpand)) {
1✔
591
                        Enumeration<JarEntry> jarEntries = jarFile.entries();
1✔
592
                        boolean foundName = (name == null);
1✔
593
                        
594
                        // loop over all of the elements looking for the match to 'name'
595
                        while (jarEntries.hasMoreElements()) {
1✔
596
                                JarEntry jarEntry = jarEntries.nextElement();
1✔
597
                                if (name == null || jarEntry.getName().startsWith(name)) {
1✔
598
                                        String entryName = jarEntry.getName();
1✔
599
                                        // trim out the name path from the name of the new file
600
                                        if (!keepFullPath && name != null) {
1✔
601
                                                entryName = entryName.replaceFirst(name, "");
1✔
602
                                        }
603
                                        
604
                                        // if it has a slash, it's in a directory
605
                                        int last = entryName.lastIndexOf('/');
1✔
606
                                        if (last >= 0) {
1✔
607
                                                File parent = new File(docBase, entryName.substring(0, last));
1✔
608
                                                if (!parent.toPath().normalize().startsWith(docBase)) {
1✔
609
                                                        throw new UnsupportedOperationException("Attempted to create directory '" + entryName + "' rejected as it attempts to write outside the chosen directory. This may be the result of a zip-slip style attack.");
1✔
610
                                                }
611
                                                parent.mkdirs();
1✔
612
                                                log.debug("Creating parent dirs: " + parent.getAbsolutePath());
1✔
613
                                        }
614
                                        // we don't want to "expand" directories or empty names
615
                                        if (entryName.endsWith("/") || "".equals(entryName)) {
1✔
UNCOV
616
                                                continue;
×
617
                                        }
618
                                        try(InputStream input = jarFile.getInputStream(jarEntry)) {
1✔
619
                                                expand(input, docBase, entryName);
1✔
620
                                        }
621
                                        foundName = true;
1✔
622
                                }
623
                        }
1✔
624
                        if (!foundName) {
1✔
625
                                log.debug("Unable to find: " + name + " in file " + fileToExpand.getAbsolutePath());
1✔
626
                        }
627
                        
628
                }
UNCOV
629
                catch (IOException e) {
×
UNCOV
630
                        log.warn("Unable to delete tmpModuleFile on error", e);
×
UNCOV
631
                        throw e;
×
632
                }
1✔
633
        }
1✔
634
        
635
        /**
636
         * Expand the given file in the given stream to a location (fileDir/name) The <code>input</code>
637
         * InputStream is not closed in this method
638
         *
639
         * @param input stream to read from
640
         * @param fileDir directory to copy to
641
         * @param name file/directory within the <code>fileDir</code> to which we expand
642
         *            <code>input</code>
643
         * @return File the file created by the expansion.
644
         * @throws IOException if an error occurred while copying
645
         */
646
        private static void expand(InputStream input, String fileDir, String name) throws IOException {
647
                log.debug("expanding: {}", name);
1✔
648

649
                File file = new File(fileDir, name);
1✔
650

651
                if (!file.toPath().normalize().startsWith(fileDir)) {
1✔
UNCOV
652
                        throw new UnsupportedOperationException("Attempted to write file '" + name + "' rejected as it attempts to write outside the chosen directory. This may be the result of a zip-slip style attack.");
×
653
                }
654
                
655
                try (FileOutputStream outStream = new FileOutputStream(file)) {
1✔
656
                        OpenmrsUtil.copyFile(input, outStream);
1✔
657
                }
658
        }
1✔
659
        
660
        /**
661
         * Downloads the contents of a URL and copies them to a string (Borrowed from oreilly)
662
         *
663
         * @param url
664
         * @return InputStream of contents
665
         * <strong>Should</strong> return a valid input stream for old module urls
666
         */
667
        public static InputStream getURLStream(URL url) {
668
                InputStream in = null;
×
669
                try {
UNCOV
670
                        URLConnection uc = url.openConnection();
×
671
                        uc.setDefaultUseCaches(false);
×
UNCOV
672
                        uc.setUseCaches(false);
×
673
                        uc.setRequestProperty("Cache-Control", "max-age=0,no-cache");
×
UNCOV
674
                        uc.setRequestProperty("Pragma", "no-cache");
×
675
                        
676
                        log.debug("Logging an attempt to connect to: " + url);
×
677
                        
UNCOV
678
                        in = openConnectionCheckRedirects(uc);
×
679
                }
UNCOV
680
                catch (IOException io) {
×
UNCOV
681
                        log.warn("io while reading: " + url, io);
×
UNCOV
682
                }
×
683
                
UNCOV
684
                return in;
×
685
        }
686
        
687
        /**
688
         * Convenience method to follow http to https redirects. Will follow a total of 5 redirects,
689
         * then fail out due to foolishness on the url's part.
690
         *
691
         * @param c the {@link URLConnection} to open
692
         * @return an {@link InputStream} that is not necessarily at the same url, possibly at a 403
693
         *         redirect.
694
         * @throws IOException
695
         * @see #getURLStream(URL)
696
         */
697
        protected static InputStream openConnectionCheckRedirects(URLConnection c) throws IOException {
698
                boolean redir;
UNCOV
699
                int redirects = 0;
×
700
                InputStream in;
701
                do {
702
                        if (c instanceof HttpURLConnection) {
×
703
                                ((HttpURLConnection) c).setInstanceFollowRedirects(false);
×
704
                        }
705
                        // We want to open the input stream before getting headers
706
                        // because getHeaderField() et al swallow IOExceptions.
707
                        in = c.getInputStream();
×
708
                        redir = false;
×
709
                        if (c instanceof HttpURLConnection) {
×
710
                                HttpURLConnection http = (HttpURLConnection) c;
×
711
                                int stat = http.getResponseCode();
×
712
                                if (stat == 300 || stat == 301 || stat == 302 || stat == 303 || stat == 305 || stat == 307) {
×
UNCOV
713
                                        URL base = http.getURL();
×
714
                                        String loc = http.getHeaderField("Location");
×
UNCOV
715
                                        URL target = null;
×
UNCOV
716
                                        if (loc != null) {
×
717
                                                target = new URL(base, loc);
×
718
                                        }
719
                                        http.disconnect();
×
720
                                        // Redirection should be allowed only for HTTP and HTTPS
721
                                        // and should be limited to 5 redirects at most.
722
                                        if (target == null || !("http".equals(target.getProtocol()) || "https".equals(target.getProtocol()))
×
723
                                                || redirects >= 5) {
UNCOV
724
                                                throw new SecurityException("illegal URL redirect");
×
725
                                        }
726
                                        redir = true;
×
727
                                        c = target.openConnection();
×
UNCOV
728
                                        redirects++;
×
729
                                }
730
                        }
UNCOV
731
                } while (redir);
×
UNCOV
732
                return in;
×
733
        }
734
        
735
        /**
736
         * Downloads the contents of a URL and copies them to a string (Borrowed from oreilly)
737
         *
738
         * @param url
739
         * @return String contents of the URL
740
         * <strong>Should</strong> return an update rdf page for old https dev urls
741
         * <strong>Should</strong> return an update rdf page for old https module urls
742
         * <strong>Should</strong> return an update rdf page for module urls
743
         */
744
        public static String getURL(URL url) {
745
                InputStream in = null;
×
UNCOV
746
                ByteArrayOutputStream out = null;
×
747
                String output = "";
×
748
                try {
UNCOV
749
                        in = getURLStream(url);
×
750
                        if (in == null) {
×
751
                                // skip this module if updateURL is not defined
752
                                return "";
×
753
                        }
754
                        
755
                        out = new ByteArrayOutputStream();
×
UNCOV
756
                        OpenmrsUtil.copyFile(in, out);
×
UNCOV
757
                        output = out.toString(StandardCharsets.UTF_8.name());
×
758
                }
759
                catch (IOException io) {
×
UNCOV
760
                        log.warn("io while reading: " + url, io);
×
761
                }
762
                finally {
763
                        try {
UNCOV
764
                                in.close();
×
765
                        }
UNCOV
766
                        catch (Exception e) { /* pass */}
×
767
                        try {
768
                                out.close();
×
769
                        }
UNCOV
770
                        catch (Exception e) { /* pass */}
×
771
                }
772
                
UNCOV
773
                return output;
×
774
        }
775
        
776
        /**
777
         * Iterates over the modules and checks each update.rdf file for an update
778
         *
779
         * @return True if an update was found for one of the modules, false if none were found
780
         * @throws ModuleException
781
         */
782
        public static Boolean checkForModuleUpdates() throws ModuleException {
783
                
UNCOV
784
                Boolean updateFound = false;
×
785
                
786
                for (Module mod : ModuleFactory.getLoadedModules()) {
×
787
                        String updateURL = mod.getUpdateURL();
×
788
                        if (StringUtils.isNotEmpty(updateURL)) {
×
789
                                try {
790
                                        // get the contents pointed to by the url
791
                                        URL url = new URL(updateURL);
×
UNCOV
792
                                        if (!url.toString().endsWith(ModuleConstants.UPDATE_FILE_NAME)) {
×
UNCOV
793
                                                log.warn("Illegal url: " + url);
×
794
                                                continue;
×
795
                                        }
UNCOV
796
                                        String content = getURL(url);
×
797
                                        
798
                                        // skip empty or invalid updates
799
                                        if ("".equals(content)) {
×
800
                                                continue;
×
801
                                        }
802
                                        
803
                                        // process and parse the contents
UNCOV
804
                                        UpdateFileParser parser = new UpdateFileParser(content);
×
UNCOV
805
                                        parser.parse();
×
806
                                        
807
                                        log.debug("Update for mod: " + mod.getModuleId() + " compareVersion result: "
×
808
                                                + compareVersion(mod.getVersion(), parser.getCurrentVersion()));
×
809
                                        
810
                                        // check the update.rdf version against the installed version
UNCOV
811
                                        if (compareVersion(mod.getVersion(), parser.getCurrentVersion()) < 0) {
×
812
                                                if (mod.getModuleId().equals(parser.getModuleId())) {
×
UNCOV
813
                                                        mod.setDownloadURL(parser.getDownloadURL());
×
UNCOV
814
                                                        mod.setUpdateVersion(parser.getCurrentVersion());
×
815
                                                        updateFound = true;
×
816
                                                } else {
UNCOV
817
                                                        log.warn("Module id does not match in update.rdf:" + parser.getModuleId());
×
818
                                                }
819
                                        } else {
820
                                                mod.setDownloadURL(null);
×
UNCOV
821
                                                mod.setUpdateVersion(null);
×
822
                                        }
823
                                }
824
                                catch (ModuleException e) {
×
UNCOV
825
                                        log.warn("Unable to get updates from update.xml", e);
×
826
                                }
UNCOV
827
                                catch (MalformedURLException e) {
×
828
                                        log.warn("Unable to form a URL object out of: " + updateURL, e);
×
UNCOV
829
                                }
×
830
                        }
UNCOV
831
                }
×
832
                
UNCOV
833
                return updateFound;
×
834
        }
835
        
836
        /**
837
         * @return true/false whether the 'allow upload' or 'allow web admin' property has been turned
838
         *         on
839
         */
840
        public static Boolean allowAdmin() {
841
                
UNCOV
842
                Properties properties = Context.getRuntimeProperties();
×
843
                String prop = properties.getProperty(ModuleConstants.RUNTIMEPROPERTY_ALLOW_UPLOAD, null);
×
UNCOV
844
                if (prop == null) {
×
UNCOV
845
                        prop = properties.getProperty(ModuleConstants.RUNTIMEPROPERTY_ALLOW_ADMIN, "false");
×
846
                }
847
                
UNCOV
848
                return "true".equals(prop);
×
849
        }
850
        
851
        /**
852
         * @see ModuleUtil#refreshApplicationContext(AbstractRefreshableApplicationContext, boolean, Module)
853
         */
854
        public static AbstractRefreshableApplicationContext refreshApplicationContext(AbstractRefreshableApplicationContext ctx) {
UNCOV
855
                return refreshApplicationContext(ctx, false, null);
×
856
        }
857
        
858
        /**
859
         * Refreshes the given application context "properly" in OpenMRS. Will first shut down the
860
         * Context and destroy the classloader, then will refresh and set everything back up again.
861
         *
862
         * @param ctx Spring application context that needs refreshing.
863
         * @param isOpenmrsStartup if this refresh is being done at application startup.
864
         * @param startedModule the module that was just started and waiting on the context refresh.
865
         * @return AbstractRefreshableApplicationContext The newly refreshed application context.
866
         */
867
        public static AbstractRefreshableApplicationContext refreshApplicationContext(AbstractRefreshableApplicationContext ctx,
868
                boolean isOpenmrsStartup, Module startedModule) {
869
                //notify all started modules that we are about to refresh the context
870
                Set<Module> startedModules = new LinkedHashSet<>(ModuleFactory.getStartedModulesInOrder());
×
871
                for (Module module : startedModules) {
×
872
                        try {
UNCOV
873
                                if (module.getModuleActivator() != null) {
×
874
                                        log.debug("Run module willRefreshContext: {}", module.getModuleId());
×
875
                                        Thread.currentThread().setContextClassLoader(ModuleFactory.getModuleClassLoader(module));
×
876
                                        module.getModuleActivator().willRefreshContext();
×
877
                                }
878
                        }
879
                        catch (Error e) {
×
880
                                log.warn("Unable to call willRefreshContext() method in the module's activator", e);
×
881
                        }
×
UNCOV
882
                }
×
883
                
884
                OpenmrsClassLoader.saveState();
×
885
                SchedulerUtil.shutdown();
×
UNCOV
886
                ServiceContext.destroyInstance();
×
887
                
888
                try {
UNCOV
889
                        ctx.stop();
×
UNCOV
890
                        ctx.close();
×
891
                }
892
                catch (Exception e) {
×
893
                        log.warn("Exception while stopping and closing context: ", e);
×
894
                        // Spring seems to be trying to refresh the context instead of /just/ stopping
895
                        // pass
896
                }
×
897
                OpenmrsClassLoader.destroyInstance();
×
UNCOV
898
                ctx.setClassLoader(OpenmrsClassLoader.getInstance());
×
899
                Thread.currentThread().setContextClassLoader(OpenmrsClassLoader.getInstance());
×
900
                
901
                log.debug("Refreshing context");
×
UNCOV
902
                ServiceContext.getInstance().startRefreshingContext();
×
903
                try {
UNCOV
904
                        ctx.refresh();
×
905
                        // TRUNK-6421: warn if XML-declared services also use annotations like @Autowired
UNCOV
906
                        logWarningsForAnnotatedXmlServices(ctx);
×
907
                        
908
                }
909
                finally {
910
                        ServiceContext.getInstance().doneRefreshingContext();
×
911
                }
912
                log.debug("Done refreshing context");
×
913
                
914
                ctx.setClassLoader(OpenmrsClassLoader.getInstance());
×
UNCOV
915
                Thread.currentThread().setContextClassLoader(OpenmrsClassLoader.getInstance());
×
916
                
UNCOV
917
                OpenmrsClassLoader.restoreState();
×
UNCOV
918
                log.debug("Startup scheduler");
×
919
                SchedulerUtil.startup(Context.getRuntimeProperties());
×
920
                
UNCOV
921
                OpenmrsClassLoader.setThreadsToNewClassLoader();
×
922
                
923
                // reload the advice points that were lost when refreshing Spring
UNCOV
924
                log.debug("Reloading advice for all started modules: {}", startedModules.size());
×
925
                
926
                try {
927
                        //The call backs in this block may need lazy loading of objects
928
                        //which will fail because we use an OpenSessionInViewFilter whose opened session
929
                        //was closed when the application context was refreshed as above.
930
                        //So we need to open another session now. TRUNK-3739
UNCOV
931
                        Context.openSessionWithCurrentUser();
×
932
                        for (Module module : startedModules) {
×
UNCOV
933
                                if (!module.isStarted()) {
×
934
                                        continue;
×
935
                                }
936
                                
937
                                ModuleFactory.loadAdvice(module);
×
938
                                try {
UNCOV
939
                                        ModuleFactory.passDaemonToken(module);
×
940
                                        
941
                                        if (module.getModuleActivator() != null) {
×
942
                                                log.debug("Run module contextRefreshed: {}", module.getModuleId());
×
943
                                                module.getModuleActivator().contextRefreshed();
×
944
                                                try {
945
                                                        //if it is system start up, call the started method for all started modules
946
                                                        if (isOpenmrsStartup) {
×
947
                                                                log.debug("Run module started: {}", module.getModuleId());
×
948
                                                                module.getModuleActivator().started();
×
949
                                                        }
950
                                                        //if refreshing the context after a user started or uploaded a new module
UNCOV
951
                                                        else if (!isOpenmrsStartup && module.equals(startedModule)) {
×
952
                                                                log.debug("Run module started: {}", module.getModuleId());
×
953
                                                                module.getModuleActivator().started();
×
954
                                                        }
955
                                                        log.debug("Done running module started: {}", module.getModuleId());
×
956
                                                }
UNCOV
957
                                                catch (Error e) {
×
UNCOV
958
                                                        log.warn("Unable to invoke started() method on the module's activator", e);
×
959
                                                        ModuleFactory.stopModule(module, true, true);
×
960
                                                }
×
961
                                        }
962
                                        
963
                                }
UNCOV
964
                                catch (Error e) {
×
965
                                        log.warn("Unable to invoke method on the module's activator ", e);
×
UNCOV
966
                                }
×
UNCOV
967
                        }
×
968
                }
969
                finally {
UNCOV
970
                        Context.closeSessionWithCurrentUser();
×
971
                }
972
                
UNCOV
973
                return ctx;
×
974
        }
975
        
976
        private static void logWarningsForAnnotatedXmlServices(AbstractRefreshableApplicationContext ctx) {
UNCOV
977
                ConfigurableListableBeanFactory factory = ctx.getBeanFactory();
×
978

979
                for (String beanName : factory.getBeanDefinitionNames()) {
×
980
                        BeanDefinition def = factory.getBeanDefinition(beanName);
×
981
                        String beanClassName = def.getBeanClassName();
×
982

983
                        if ("org.springframework.transaction.interceptor.TransactionProxyFactoryBean".equals(beanClassName)) {
×
984
                                PropertyValue target = def.getPropertyValues().getPropertyValue("target");
×
985
                                if (target != null) {
×
986
                                        if (target.getValue() instanceof BeanDefinitionHolder) {
×
987
                                                def = ((BeanDefinitionHolder) target.getValue()).getBeanDefinition();
×
UNCOV
988
                                                beanClassName = def.getBeanClassName();
×
UNCOV
989
                                        } else if (target.getValue() instanceof RuntimeBeanReference) {
×
UNCOV
990
                                                beanName = ((RuntimeBeanReference) target.getValue()).getBeanName();
×
UNCOV
991
                                                def = factory.getBeanDefinition(beanName);
×
UNCOV
992
                                                beanClassName = def.getBeanClassName();
×
993
                                        }
994
                                }
995
                        } else {
996
                                continue;
997
                        }
998
                        
999
                        if (beanClassName == null) {
×
1000
                                log.debug("No classname for bean {}", beanName);
×
UNCOV
1001
                                continue;
×
1002
                        }
1003
                        
UNCOV
1004
                        int proxySuffix = beanClassName.indexOf("$$");
×
UNCOV
1005
                        if (proxySuffix != -1) {
×
1006
                                // Get rid of proxy suffix
1007
                                beanClassName = beanClassName.substring(0, proxySuffix);
×
1008
                        }
1009

1010
                        Class<?> beanClass;
1011
                        try {
1012
                                beanClass = OpenmrsClassLoader.getInstance().loadClass(beanClassName);
×
1013
                        }
1014
                        catch (ClassNotFoundException e) {
×
UNCOV
1015
                                log.debug("Unable to load class {} for bean {}", beanClassName, beanName, e);
×
1016
                                continue;
×
1017
                        }
×
1018

1019
                        boolean hasAutowired = false;
×
1020
                        
UNCOV
1021
                        for (Field field : beanClass.getDeclaredFields()) {
×
UNCOV
1022
                                if (AnnotationUtils.getAnnotation(field, Autowired.class) != null) {
×
1023
                                        hasAutowired = true;
×
1024
                                        break;
×
1025
                                }
1026
                        }
1027
                        
UNCOV
1028
                        if (!hasAutowired) {
×
UNCOV
1029
                                for (Constructor<?> ctor : beanClass.getDeclaredConstructors()) {
×
UNCOV
1030
                                        if (AnnotationUtils.getAnnotation(ctor, Autowired.class) != null) {
×
UNCOV
1031
                                                hasAutowired = true;
×
1032
                                                break;
×
1033
                                        }
1034
                                }
1035
                        }
1036
                        
UNCOV
1037
                        if (!hasAutowired) {
×
UNCOV
1038
                                for (Method method : beanClass.getDeclaredMethods()) {
×
UNCOV
1039
                                        if (AnnotationUtils.getAnnotation(method, Autowired.class) != null) {
×
UNCOV
1040
                                                hasAutowired = true;
×
1041
                                                break;
×
1042
                                        }
1043
                                }
1044
                        }
1045

UNCOV
1046
                        if (!hasAutowired) {
×
UNCOV
1047
                                continue;
×
1048
                        }
1049
                        
UNCOV
1050
                        log.warn("Service bean '{}' ({}) appears to be declared via XML " +
×
1051
                                        "and also uses @Autowired annotations. This combination can lead to " +
1052
                                        "slow context refresh when mixing XML-declared services with " +
1053
                                        "annotation-based injection. Please remove @Autowired annotations from the bean. See TRUNK-6363.",
1054
                                beanName, beanClassName);
1055
                }
UNCOV
1056
        }
×
1057
        
1058
        /**
1059
         * Looks at the &lt;moduleid&gt;.mandatory properties and at the currently started modules to make
1060
         * sure that all mandatory modules have been started successfully.
1061
         *
1062
         * @throws ModuleException if a mandatory module isn't started
1063
         * <strong>Should</strong> throw ModuleException if a mandatory module is not started
1064
         */
1065
        protected static void checkMandatoryModulesStarted() throws ModuleException {
1066
                
1067
                List<String> mandatoryModuleIds = getMandatoryModules();
1✔
1068
                Set<String> startedModuleIds = ModuleFactory.getStartedModulesMap().keySet();
1✔
1069
                
1070
                mandatoryModuleIds.removeAll(startedModuleIds);
1✔
1071
                
1072
                // any module ids left in the list are not started
1073
                if (!mandatoryModuleIds.isEmpty()) {
1✔
1074
                        throw new MandatoryModuleException(mandatoryModuleIds);
1✔
1075
                }
1076
        }
1✔
1077
        
1078
        /**
1079
         * Returns all modules that are marked as mandatory. Currently this means there is a
1080
         * &lt;moduleid&gt;.mandatory=true global property.
1081
         *
1082
         * @return list of modules ids for mandatory modules
1083
         * <strong>Should</strong> return mandatory module ids
1084
         */
1085
        public static List<String> getMandatoryModules() {
1086
                
1087
                List<String> mandatoryModuleIds = new ArrayList<>();
1✔
1088
                
1089
                try {
1090
                        List<GlobalProperty> props = Context.getAdministrationService().getGlobalPropertiesBySuffix(".mandatory");
1✔
1091
                        
1092
                        for (GlobalProperty prop : props) {
1✔
1093
                                if ("true".equalsIgnoreCase(prop.getPropertyValue())) {
1✔
1094
                                        mandatoryModuleIds.add(prop.getProperty().replace(".mandatory", ""));
1✔
1095
                                }
1096
                        }
1✔
1097
                }
UNCOV
1098
                catch (Exception e) {
×
UNCOV
1099
                        log.warn("Unable to get the mandatory module list", e);
×
1100
                }
1✔
1101
                
1102
                return mandatoryModuleIds;
1✔
1103
        }
1104
        
1105
        /**
1106
         * <pre>
1107
         * Gets the module that should handle a path. The path you pass in should be a module id (in
1108
         * path format, i.e. /ui/springmvc, not ui.springmvc) followed by a resource. Something like
1109
         * the following:
1110
         *   /ui/springmvc/css/ui.css
1111
         *
1112
         * The first running module out of the following would be returned:
1113
         *   ui.springmvc.css
1114
         *   ui.springmvc
1115
         *   ui
1116
         * </pre>
1117
         *
1118
         * @param path
1119
         * @return the running module that matches the most of the given path
1120
         * <strong>Should</strong> handle ui springmvc css ui dot css when ui dot springmvc module is running
1121
         * <strong>Should</strong> handle ui springmvc css ui dot css when ui module is running
1122
         * <strong>Should</strong> return null for ui springmvc css ui dot css when no relevant module is running
1123
         */
1124
        public static Module getModuleForPath(String path) {
1125
                int ind = path.lastIndexOf('/');
1✔
1126
                if (ind <= 0) {
1✔
UNCOV
1127
                        throw new IllegalArgumentException(
×
1128
                                "Input must be /moduleId/resource. Input needs a / after the first character: " + path);
1129
                }
1130
                String moduleId = path.startsWith("/") ? path.substring(1, ind) : path.substring(0, ind);
1✔
1131
                moduleId = moduleId.replace('/', '.');
1✔
1132
                // iterate over progressively shorter module ids
1133
                while (true) {
1134
                        Module mod = ModuleFactory.getStartedModuleById(moduleId);
1✔
1135
                        if (mod != null) {
1✔
1136
                                return mod;
1✔
1137
                        }
1138
                        // try the next shorter module id
1139
                        ind = moduleId.lastIndexOf('.');
1✔
1140
                        if (ind < 0) {
1✔
1141
                                break;
1✔
1142
                        }
1143
                        moduleId = moduleId.substring(0, ind);
1✔
1144
                }
1✔
1145
                return null;
1✔
1146
        }
1147
        
1148
        /**
1149
         * Takes a global path and returns the local path within the specified module. For example
1150
         * calling this method with the path "/ui/springmvc/css/ui.css" and the ui.springmvc module, you
1151
         * would get "/css/ui.css".
1152
         *
1153
         * @param module
1154
         * @param path
1155
         * @return local path
1156
         * <strong>Should</strong> handle ui springmvc css ui dot css example
1157
         */
1158
        public static String getPathForResource(Module module, String path) {
1159
                if (path.startsWith("/")) {
1✔
1160
                        path = path.substring(1);
1✔
1161
                }
1162
                return path.substring(module.getModuleIdAsPath().length());
1✔
1163
        }
1164
        
1165
        /**
1166
         * This loops over all FILES in this jar to get the package names. If there is an empty
1167
         * directory in this jar it is not returned as a providedPackage.
1168
         *
1169
         * @param file jar file to look into
1170
         * @return list of strings of package names in this jar
1171
         */
1172
        public static Collection<String> getPackagesFromFile(File file) {
1173
                
1174
                // End early if we're given a non jar file
1175
                if (!file.getName().endsWith(".jar")) {
1✔
1176
                        return Collections.emptySet();
1✔
1177
                }
1178
                
1179
                Set<String> packagesProvided = new HashSet<>();
1✔
1180
                
1181
                JarFile jar = null;
1✔
1182
                try {
1183
                        jar = new JarFile(file);
1✔
1184
                        
1185
                        Enumeration<JarEntry> jarEntries = jar.entries();
1✔
1186
                        while (jarEntries.hasMoreElements()) {
1✔
1187
                                JarEntry jarEntry = jarEntries.nextElement();
1✔
1188
                                if (jarEntry.isDirectory()) {
1✔
1189
                                        // skip over directory entries, we only care about files
1190
                                        continue;
1✔
1191
                                }
1192
                                String name = jarEntry.getName();
1✔
1193
                                
1194
                                // Skip over some folders in the jar/omod
1195
                                if (name.startsWith("lib") || name.startsWith("META-INF") || name.startsWith("web/module")) {
1✔
1196
                                        continue;
1✔
1197
                                }
1198
                                
1199
                                Integer indexOfLastSlash = name.lastIndexOf("/");
1✔
1200
                                if (indexOfLastSlash <= 0) {
1✔
1201
                                        continue;
1✔
1202
                                }
1203
                                String packageName = name.substring(0, indexOfLastSlash);
1✔
1204
                                
1205
                                packageName = packageName.replaceAll("/", ".");
1✔
1206
                                
1207
                                if (packagesProvided.add(packageName) && log.isTraceEnabled()) {
1✔
UNCOV
1208
                                        log.trace("Adding module's jarentry with package: " + packageName);
×
1209
                                }
1210
                        }
1✔
1211
                        
1212
                        jar.close();
1✔
1213
                }
UNCOV
1214
                catch (IOException e) {
×
UNCOV
1215
                        log.error("Error while reading file: " + file.getAbsolutePath(), e);
×
1216
                }
1217
                finally {
1218
                        if (jar != null) {
1✔
1219
                                try {
1220
                                        jar.close();
1✔
1221
                                }
UNCOV
1222
                                catch (IOException e) {
×
1223
                                        // Ignore quietly
1224
                                }
1✔
1225
                        }
1226
                }
1227
                
1228
                return packagesProvided;
1✔
1229
        }
1230
        
1231
        /**
1232
         * Get a resource as from the module's api jar. Api jar should be in the omod's lib folder.
1233
         * 
1234
         * @param jarFile omod file loaded as jar
1235
         * @param moduleId id of the module
1236
         * @param version version of the module
1237
         * @param resource name of a resource from the api jar
1238
         * @return resource as an input stream or <code>null</code> if resource cannot be loaded
1239
         * <strong>Should</strong> load file from api as input stream
1240
         * <strong>Should</strong> return null if api is not found
1241
         * <strong>Should</strong> return null if file is not found in api
1242
         */
1243
        public static InputStream getResourceFromApi(JarFile jarFile, String moduleId, String version, String resource) {
1244
                String apiLocation = "lib/" + moduleId + "-api-" + version + ".jar";
1✔
1245
                return getResourceFromInnerJar(jarFile, apiLocation, resource);
1✔
1246
        }
1247
        
1248
        /**
1249
         * Load resource from a jar inside a jar.
1250
         * 
1251
         * @param outerJarFile jar file that contains a jar file
1252
         * @param innerJarFileLocation inner jar file location relative to the outer jar
1253
         * @param resource path to a resource relative to the inner jar
1254
         * @return resource from the inner jar as an input stream or <code>null</code> if resource cannot be loaded
1255
         */
1256
        private static InputStream getResourceFromInnerJar(JarFile outerJarFile, String innerJarFileLocation, String resource) {
1257
                File tempFile = null;
1✔
1258
                FileOutputStream tempOut = null;
1✔
1259
                JarFile innerJarFile = null;
1✔
1260
                InputStream innerInputStream = null;
1✔
1261
                try {
1262
                        tempFile = File.createTempFile("tempFile", "jar");
1✔
1263
                        tempOut = new FileOutputStream(tempFile);
1✔
1264
                        ZipEntry innerJarFileEntry = outerJarFile.getEntry(innerJarFileLocation);
1✔
1265
                        if (innerJarFileEntry != null) {
1✔
1266
                                IOUtils.copy(outerJarFile.getInputStream(innerJarFileEntry), tempOut);
1✔
1267
                                innerJarFile = new JarFile(tempFile);
1✔
1268
                                ZipEntry targetEntry = innerJarFile.getEntry(resource);
1✔
1269
                                if (targetEntry != null) {
1✔
1270
                                        // clone InputStream to make it work after the innerJarFile is closed
1271
                                        innerInputStream = innerJarFile.getInputStream(targetEntry);
1✔
1272
                                        byte[] byteArray = IOUtils.toByteArray(innerInputStream);
1✔
1273
                                        return new ByteArrayInputStream(byteArray);
1✔
1274
                                }
1275
                        }
1276
                }
UNCOV
1277
                catch (IOException e) {
×
UNCOV
1278
                        log.error("Unable to get '" + resource + "' from '" + innerJarFileLocation + "' of '" + outerJarFile.getName()
×
1279
                                + "'", e);
1280
                }
1281
                finally {
1282
                        IOUtils.closeQuietly(tempOut);
1✔
1283
                        IOUtils.closeQuietly(innerInputStream);
1✔
1284

1285
                        // close inner jar file before attempting to delete temporary file
1286
                        try {
1287
                                if (innerJarFile != null) {
1✔
1288
                                        innerJarFile.close();
1✔
1289
                                }
1290
                        }
UNCOV
1291
                        catch (IOException e) {
×
1292
                                log.warn("Unable to close inner jarfile: " + innerJarFile, e);
×
1293
                        }
1✔
1294

1295
                        // delete temporary file
1296
                        if (tempFile != null && !tempFile.delete()) {
1✔
UNCOV
1297
                                log.warn("Could not delete temporary jarfile: " + tempFile);
×
1298
                        }
1299
                }
1300
                return null;
1✔
1301
        }
1302
        
1303
        /**
1304
         * Gets the root folder of a module's sources during development
1305
         * 
1306
         * @param moduleId the module id
1307
         * @return the module's development folder is specified, else null
1308
         */
1309
        public static File getDevelopmentDirectory(String moduleId) {
1310
                String directory = System.getProperty(moduleId + ".development.directory");
1✔
1311
                if (StringUtils.isNotBlank(directory)) {
1✔
UNCOV
1312
                        return new File(directory);
×
1313
                }
1314
                
1315
                return null;
1✔
1316
        }
1317
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc