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

openmrs / openmrs-core / 20851422941

09 Jan 2026 12:07PM UTC coverage: 63.315% (+0.02%) from 63.297%
20851422941

push

github

rkorytkowski
TRUNK-6509: Do not expand jars every restart

41 of 55 new or added lines in 6 files covered. (74.55%)

19 existing lines in 3 files now uncovered.

23084 of 36459 relevant lines covered (63.31%)

0.63 hits per line

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

44.62
/api/src/main/java/org/openmrs/util/OpenmrsUtil.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.util;
11

12
import java.io.BufferedInputStream;
13
import java.io.BufferedReader;
14
import java.io.ByteArrayInputStream;
15
import java.io.ByteArrayOutputStream;
16
import java.io.Closeable;
17
import java.io.File;
18
import java.io.FileInputStream;
19
import java.io.FileNotFoundException;
20
import java.io.FileOutputStream;
21
import java.io.IOException;
22
import java.io.InputStream;
23
import java.io.InputStreamReader;
24
import java.io.OutputStream;
25
import java.io.OutputStreamWriter;
26
import java.io.UnsupportedEncodingException;
27
import java.lang.reflect.Field;
28
import java.lang.reflect.Method;
29
import java.net.MalformedURLException;
30
import java.net.URL;
31
import java.nio.charset.Charset;
32
import java.nio.charset.StandardCharsets;
33
import java.nio.file.Paths;
34
import java.sql.Timestamp;
35
import java.text.DateFormat;
36
import java.text.SimpleDateFormat;
37
import java.util.ArrayList;
38
import java.util.Arrays;
39
import java.util.Calendar;
40
import java.util.Collection;
41
import java.util.Collections;
42
import java.util.Date;
43
import java.util.HashMap;
44
import java.util.HashSet;
45
import java.util.Iterator;
46
import java.util.LinkedHashSet;
47
import java.util.LinkedList;
48
import java.util.List;
49
import java.util.Locale;
50
import java.util.Map;
51
import java.util.Properties;
52
import java.util.Random;
53
import java.util.Set;
54
import java.util.StringTokenizer;
55
import java.util.jar.JarFile;
56
import java.util.regex.Matcher;
57
import java.util.regex.Pattern;
58
import java.util.regex.PatternSyntaxException;
59
import java.util.stream.Collectors;
60
import java.util.zip.ZipEntry;
61

62
import javax.activation.MimetypesFileTypeMap;
63
import javax.xml.transform.OutputKeys;
64
import javax.xml.transform.Transformer;
65
import javax.xml.transform.TransformerException;
66
import javax.xml.transform.TransformerFactory;
67
import javax.xml.transform.dom.DOMSource;
68
import javax.xml.transform.stream.StreamResult;
69

70
import org.apache.commons.io.IOUtils;
71
import org.apache.commons.lang3.StringUtils;
72
import org.openmrs.Cohort;
73
import org.openmrs.Concept;
74
import org.openmrs.ConceptNumeric;
75
import org.openmrs.ConceptReferenceRange;
76
import org.openmrs.Drug;
77
import org.openmrs.EncounterType;
78
import org.openmrs.Form;
79
import org.openmrs.Location;
80
import org.openmrs.Obs;
81
import org.openmrs.PersonAttributeType;
82
import org.openmrs.Program;
83
import org.openmrs.ProgramWorkflowState;
84
import org.openmrs.User;
85
import org.openmrs.annotation.AddOnStartup;
86
import org.openmrs.annotation.HasAddOnStartupPrivileges;
87
import org.openmrs.annotation.Logging;
88
import org.openmrs.api.APIException;
89
import org.openmrs.api.AdministrationService;
90
import org.openmrs.api.ConceptService;
91
import org.openmrs.api.InvalidCharactersPasswordException;
92
import org.openmrs.api.PasswordException;
93
import org.openmrs.api.ShortPasswordException;
94
import org.openmrs.api.WeakPasswordException;
95
import org.openmrs.api.context.Context;
96
import org.openmrs.logging.OpenmrsLoggingUtil;
97
import org.openmrs.module.ModuleException;
98
import org.openmrs.module.ModuleFactory;
99
import org.openmrs.propertyeditor.CohortEditor;
100
import org.openmrs.propertyeditor.ConceptEditor;
101
import org.openmrs.propertyeditor.DrugEditor;
102
import org.openmrs.propertyeditor.EncounterTypeEditor;
103
import org.openmrs.propertyeditor.FormEditor;
104
import org.openmrs.propertyeditor.LocationEditor;
105
import org.openmrs.propertyeditor.PersonAttributeTypeEditor;
106
import org.openmrs.propertyeditor.ProgramEditor;
107
import org.openmrs.propertyeditor.ProgramWorkflowStateEditor;
108
import org.openmrs.validator.ObsValidator;
109
import org.slf4j.LoggerFactory;
110
import org.slf4j.MarkerFactory;
111
import org.springframework.beans.propertyeditors.CustomDateEditor;
112
import org.springframework.context.NoSuchMessageException;
113
import org.w3c.dom.Document;
114
import org.w3c.dom.DocumentType;
115

116
/**
117
 * Utility methods used in openmrs
118
 */
119
public class OpenmrsUtil {
120
        private OpenmrsUtil() {
121
        }
122
        
123
        private static volatile MimetypesFileTypeMap mimetypesFileTypeMap = null;
1✔
124
        
125
        private static org.slf4j.Logger log = LoggerFactory.getLogger(OpenmrsUtil.class);
1✔
126
        
127
        private static Map<Locale, SimpleDateFormat> dateFormatCache = new HashMap<>();
1✔
128
        
129
        private static Map<Locale, SimpleDateFormat> timeFormatCache = new HashMap<>();
1✔
130
        
131
        /**
132
         * Compares origList to newList returning map of differences
133
         * 
134
         * @param origList
135
         * @param newList
136
         * @return [List toAdd, List toDelete] with respect to origList
137
         */
138
        public static <E> Collection<Collection<E>> compareLists(Collection<E> origList, Collection<E> newList) {        
139
                Collection<Collection<E>> returnList = new ArrayList<>();
1✔
140
                
141
                Collection<E> toAdd = new LinkedList<>();
1✔
142
                Collection<E> toDel = new LinkedList<>();
1✔
143
                
144
                // loop over the new list.
145
                for (E currentNewListObj : newList) {
1✔
146
                        // loop over the original list
147
                        boolean foundInList = false;
1✔
148
                        for (E currentOrigListObj : origList) {
1✔
149
                                // checking if the current new list object is in the original
150
                                // list
151
                                if (currentNewListObj.equals(currentOrigListObj)) {
1✔
152
                                        foundInList = true;
1✔
153
                                        origList.remove(currentOrigListObj);
1✔
154
                                        break;
1✔
155
                                }
156
                        }
1✔
157
                        if (!foundInList) {
1✔
158
                                toAdd.add(currentNewListObj);
1✔
159
                        }
160
                        
161
                        // all found new objects were removed from the orig list,
162
                        // leaving only objects needing to be removed
163
                        toDel = origList;
1✔
164
                        
165
                }
1✔
166
                
167
                returnList.add(toAdd);
1✔
168
                returnList.add(toDel);
1✔
169
                
170
                return returnList;
1✔
171
        }
172
        
173
        public static boolean isStringInArray(String str, String[] arr) {
174
                if (str != null && arr != null) {
1✔
175
                        for (String anArr : arr) {
1✔
176
                                if (str.equals(anArr)) {
1✔
177
                                        return true;
1✔
178
                                }
179
                        }
180
                }
181
                return false;
1✔
182
        }
183
        
184
        public static Boolean isInNormalNumericRange(Float value, ConceptNumeric concept) {
185
                if (concept.getHiNormal() == null || concept.getLowNormal() == null) {
1✔
186
                        return false;
1✔
187
                }
188
                return (value <= concept.getHiNormal() && value >= concept.getLowNormal());
1✔
189
        }
190
        
191
        public static Boolean isInCriticalNumericRange(Float value, ConceptNumeric concept) {
192
                if (concept.getHiCritical() == null || concept.getLowCritical() == null) {
1✔
193
                        return false;
1✔
194
                }
195
                return (value <= concept.getHiCritical() && value >= concept.getLowCritical());
1✔
196
        }
197
        
198
        public static Boolean isInAbsoluteNumericRange(Float value, ConceptNumeric concept) {
199
                if (concept.getHiAbsolute() == null || concept.getLowAbsolute() == null) {
1✔
200
                        return false;
1✔
201
                }
202
                return (value <= concept.getHiAbsolute() && value >= concept.getLowAbsolute());
1✔
203
        }
204
        
205
        public static Boolean isValidNumericValue(Float value, ConceptNumeric concept) {
206
                if (concept.getHiAbsolute() == null || concept.getLowAbsolute() == null) {
1✔
207
                        return true;
1✔
208
                }
209
                return (value <= concept.getHiAbsolute() && value >= concept.getLowAbsolute());
1✔
210
        }
211
        
212
        /**
213
         * Return a string representation of the given file
214
         * 
215
         * @param file
216
         * @return String file contents
217
         * @throws IOException
218
         */
219
        public static String getFileAsString(File file) throws IOException {
220
                StringBuilder fileData = new StringBuilder(1000);
×
221
                BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
×
222
                char[] buf = new char[1024];
×
223
                int numRead;
224
                while ((numRead = reader.read(buf)) != -1) {
×
225
                        String readData = String.valueOf(buf, 0, numRead);
×
226
                        fileData.append(readData);
×
227
                        buf = new char[1024];
×
228
                }
×
229
                reader.close();
×
230
                return fileData.toString();
×
231
        }
232
        
233
        /**
234
         * Return a byte array representation of the given file
235
         * 
236
         * @param file
237
         * @return byte[] file contents
238
         * @throws IOException
239
         */
240
        public static byte[] getFileAsBytes(File file) throws IOException {
241
                FileInputStream fileInputStream = null;
×
242
                try {
243
                        fileInputStream = new FileInputStream(file);
×
244
                        byte[] b = new byte[fileInputStream.available()];
×
245
                        fileInputStream.read(b);
×
246
                        return b;
×
247
                }
248
                catch (Exception e) {
×
249
                        log.error("Unable to get file as byte array", e);
×
250
                }
251
                finally {
252
                        if (fileInputStream != null) {
×
253
                                try {
254
                                        fileInputStream.close();
×
255
                                }
256
                                catch (IOException io) {
×
257
                                        log.warn("Couldn't close fileInputStream: " + io);
×
258
                                }
×
259
                        }
260
                }
261
                
262
                return null;
×
263
        }
264
        
265
        /**
266
         * Copy file from inputStream onto the outputStream inputStream is not closed in this method
267
         * outputStream /is/ closed at completion of this method
268
         * 
269
         * @param inputStream Stream to copy from
270
         * @param outputStream Stream/location to copy to
271
         * @throws IOException thrown if an error occurs during read/write
272
         * <strong>Should</strong> not copy the outputstream if outputstream is null
273
         * <strong>Should</strong> not copy the outputstream if inputstream is null
274
         * <strong>Should</strong> copy inputstream to outputstream and close the outputstream
275
         */
276
        public static void copyFile(InputStream inputStream, OutputStream outputStream) throws IOException {
277
                if (inputStream == null || outputStream == null) {
1✔
278
                        if (outputStream != null) {
1✔
279
                                IOUtils.closeQuietly(outputStream);
1✔
280
                        }
281
                        return;
1✔
282
                }
283
                
284
                try {
285
                        IOUtils.copy(inputStream, outputStream);
1✔
286
                }
287
                finally {
288
                        IOUtils.closeQuietly(outputStream);
1✔
289
                }
290
        }
1✔
291
        
292
        /**
293
         * Get mime type of the given file
294
         *
295
         * @param file
296
         * @return mime type
297
         */
298
        public static String getFileMimeType(File file) {
299
                if (mimetypesFileTypeMap == null) {
×
300
                        synchronized (OpenmrsUtil.class) {
×
301
                                mimetypesFileTypeMap = new MimetypesFileTypeMap();
×
302
                        }
×
303
                }
304
                return mimetypesFileTypeMap.getContentType(file);
×
305
        }
306
        
307
        /**
308
         * Look for a file named <code>filename</code> in folder
309
         * 
310
         * @param folder
311
         * @param filename
312
         * @return true/false whether filename exists in folder
313
         */
314
        public static boolean folderContains(File folder, String filename) {
315
                if (folder == null) {
×
316
                        return false;
×
317
                }
318
                if (!folder.isDirectory()) {
×
319
                        return false;
×
320
                }
321
                File[] files = folder.listFiles();
×
322
                if (files == null) {
×
323
                        return false;
×
324
                }
325
                
326
                for (File f : files) {
×
327
                        if (f.getName().equals(filename)) {
×
328
                                return true;
×
329
                        }
330
                }
331
                return false;
×
332
        }
333
        
334
        /**
335
         * These are the privileges that are required by OpenMRS. This looks for privileges marked as
336
         * {@link AddOnStartup} to know which privs, upon startup or loading of a module, to insert into
337
         * the database if they do not exist already. These privileges are not allowed to be deleted.
338
         * They are marked as 'locked' in the administration screens.
339
         * 
340
         * @return privileges core to the system
341
         * @see PrivilegeConstants
342
         * @see Context#checkCoreDataset()
343
         */
344
        public static Map<String, String> getCorePrivileges() {
345
                Map<String, String> corePrivileges = new HashMap<>();
1✔
346
                
347
                // TODO getCorePrivileges() is called so so many times that getClassesWithAnnotation() better do some catching.
348
                Set<Class<?>> classes = OpenmrsClassScanner.getInstance().getClassesWithAnnotation(HasAddOnStartupPrivileges.class);
1✔
349
                
350
                for (Class cls : classes) {
1✔
351
                        Field[] flds = cls.getDeclaredFields();
1✔
352
                        for (Field fld : flds) {
1✔
353
                                String fieldValue = null;
1✔
354
                                
355
                                AddOnStartup privilegeAnnotation = fld.getAnnotation(AddOnStartup.class);
1✔
356
                                if (null == privilegeAnnotation) {
1✔
357
                                        continue;
1✔
358
                                }
359
                                if (!privilegeAnnotation.core()) {
1✔
360
                                        continue;
×
361
                                }
362
                                
363
                                try {
364
                                        fieldValue = (String) fld.get(null);
1✔
365
                                }
366
                                catch (IllegalAccessException e) {
×
367
                                        log.error("Field is inaccessible.", e);
×
368
                                }
1✔
369
                                corePrivileges.put(fieldValue, privilegeAnnotation.description());
1✔
370
                        }
371
                }
1✔
372
                
373
                // always add the module core privileges back on
374
                for (org.openmrs.Privilege privilege : ModuleFactory.getPrivileges()) {
1✔
375
                        corePrivileges.put(privilege.getPrivilege(), privilege.getDescription());
×
376
                }
×
377
                
378
                return corePrivileges;
1✔
379
        }
380
        
381
        /**
382
         * All roles returned by this method are inserted into the database if they do not exist
383
         * already. These roles are also forbidden to be deleted from the administration screens.
384
         * 
385
         * @return roles that are core to the system
386
         */
387
        public static Map<String, String> getCoreRoles() {
388
                Map<String, String> roles = new HashMap<>();
1✔
389
                
390
                Field[] flds = RoleConstants.class.getDeclaredFields();
1✔
391
                for (Field fld : flds) {
1✔
392
                        String fieldValue = null;
1✔
393
                        
394
                        AddOnStartup roleAnnotation = fld.getAnnotation(AddOnStartup.class);
1✔
395
                        if (null == roleAnnotation) {
1✔
396
                                continue;
1✔
397
                        }
398
                        if (!roleAnnotation.core()) {
1✔
399
                                continue;
×
400
                        }
401
                        
402
                        try {
403
                                fieldValue = (String) fld.get(null);
1✔
404
                        }
405
                        catch (IllegalAccessException e) {
×
406
                                log.error("Field is inaccessible.", e);
×
407
                        }
1✔
408
                        roles.put(fieldValue, roleAnnotation.description());
1✔
409
                }
410
                
411
                return roles;
1✔
412
        }
413
        
414
        /**
415
         * Initialize global settings Find and load modules
416
         * 
417
         * @param p properties from runtime configuration
418
         */
419
        public static void startup(Properties p) {
420
                
421
                // Override global OpenMRS constants if specified by the user
422
                
423
                // Allow for "demo" mode where patient data is obscured
424
                String val = p.getProperty("obscure_patients", null);
×
425
                if (val != null && "true".equalsIgnoreCase(val)) {
×
426
                        OpenmrsConstants.OBSCURE_PATIENTS = true;
×
427
                }
428
                
429
                val = p.getProperty("obscure_patients.family_name", null);
×
430
                if (val != null) {
×
431
                        OpenmrsConstants.OBSCURE_PATIENTS_FAMILY_NAME = val;
×
432
                }
433
                
434
                val = p.getProperty("obscure_patients.given_name", null);
×
435
                if (val != null) {
×
436
                        OpenmrsConstants.OBSCURE_PATIENTS_GIVEN_NAME = val;
×
437
                }
438
                
439
                val = p.getProperty("obscure_patients.middle_name", null);
×
440
                if (val != null) {
×
441
                        OpenmrsConstants.OBSCURE_PATIENTS_MIDDLE_NAME = val;
×
442
                }
443
                
444
                // Override the default "openmrs" database name
445
                val = p.getProperty("connection.database_name", null);
×
446
                if (val == null) {
×
447
                        // the database name wasn't supplied explicitly, guess it
448
                        // from the connection string
449
                        val = p.getProperty("connection.url", null);
×
450
                        
451
                        if (val != null) {
×
452
                                try {
453
                                        int endIndex = val.lastIndexOf("?");
×
454
                                        if (endIndex == -1) {
×
455
                                                endIndex = val.length();
×
456
                                        }
457
                                        int startIndex = val.lastIndexOf("/", endIndex);
×
458
                                        val = val.substring(startIndex + 1, endIndex);
×
459
                                        OpenmrsConstants.DATABASE_NAME = val;
×
460
                                }
461
                                catch (Exception e) {
×
462
                                        log.error(MarkerFactory.getMarker("FATAL"), "Database name cannot be configured from 'connection.url' ."
×
463
                                                + "Either supply 'connection.database_name' or correct the url",
464
                                            e);
465
                                }
×
466
                        }
467
                }
468
                
469
                // set the business database name
470
                val = p.getProperty("connection.database_business_name", null);
×
471
                if (val == null) {
×
472
                        val = OpenmrsConstants.DATABASE_NAME;
×
473
                }
474
                OpenmrsConstants.DATABASE_BUSINESS_NAME = val;
×
475
        }
×
476
        
477
        /**
478
         * Gets the in-memory log appender. This method needed to be added as it is much more difficult to
479
         * get a specific appender in the Log4J2 architecture. This method is called in places where we need
480
         * to display logging message.
481
         *
482
         * @since 2.4.0
483
         * @deprecated As of 2.4.4, 2.5.1, and 2.6.0; replaced by {@link OpenmrsLoggingUtil#getMemoryAppender()} instead
484
         */
485
        @Deprecated
486
        public static MemoryAppender getMemoryAppender() {
487
                return new MemoryAppender(OpenmrsLoggingUtil.getMemoryAppender());
×
488
        }
489
        
490
        /**
491
         * Set the org.openmrs log4j logger's level if global property log.level.openmrs (
492
         * OpenmrsConstants.GLOBAL_PROPERTY_LOG_LEVEL ) exists. Valid values for global property are
493
         * trace, debug, info, warn, error or fatal.
494
         * 
495
         * @deprecated As of 2.4.4, 2.5.1, and 2.6.0; replaced by {@link OpenmrsLoggingUtil#applyLogLevels()}
496
         */
497
        @Logging(ignore = true)
498
        @Deprecated
499
        public static void applyLogLevels() {
500
                OpenmrsLoggingUtil.applyLogLevels();
1✔
501
        }
1✔
502
        
503
        /**
504
         * Setup root level log appenders.
505
         *
506
         * @since 1.9.2
507
         * @deprecated As of 2.4.4, 2.5.1, and 2.6.0; replaced by {@link OpenmrsLoggingUtil#reloadLoggingConfiguration()}
508
         */
509
        @Deprecated
510
        public static void setupLogAppenders() {
511
                OpenmrsLoggingUtil.reloadLoggingConfiguration();
×
512
        }
×
513
        
514
        /**
515
         * Set the log4j log level for class <code>logClass</code> to <code>logLevel</code>.
516
         * 
517
         * @param logClass optional string giving the class level to change. Defaults to
518
         *            OpenmrsConstants.LOG_CLASS_DEFAULT . Should be something like org.openmrs.___
519
         * @param logLevel one of OpenmrsConstants.LOG_LEVEL_*
520
         *                 
521
         * @deprecated As of 2.4.4, 2.5.1, and 2.6.0; replaced by {@link OpenmrsLoggingUtil#applyLogLevel(String, String)}
522
         */
523
        @Deprecated
524
        public static void applyLogLevel(String logClass, String logLevel) {
525
                OpenmrsLoggingUtil.applyLogLevel(logClass, logLevel);
1✔
526
        }
1✔
527
        
528
        /**
529
         * Takes a String like "size=compact|order=date" and returns a Map&lt;String,String&gt; from the
530
         * keys to the values.
531
         * 
532
         * @param paramList <code>String</code> with a list of parameters
533
         * @return Map&lt;String, String&gt; of the parameters passed
534
         */
535
        public static Map<String, String> parseParameterList(String paramList) {
536
                Map<String, String> ret = new HashMap<>();
1✔
537
                if (paramList != null && paramList.length() > 0) {
1✔
538
                        String[] args = paramList.split("\\|");
1✔
539
                        for (String s : args) {
1✔
540
                                int ind = s.indexOf('=');
1✔
541
                                if (ind <= 0) {
1✔
542
                                        throw new IllegalArgumentException(
1✔
543
                                                "Misformed argument in dynamic page specification string: '" + s + "' is not 'key=value'.");
544
                                }
545
                                String name = s.substring(0, ind);
1✔
546
                                String value = s.substring(ind + 1);
1✔
547
                                ret.put(name, value);
1✔
548
                        }
549
                }
550
                return ret;
1✔
551
        }
552
        
553
        public static <Arg1, Arg2 extends Arg1> boolean nullSafeEquals(Arg1 d1, Arg2 d2) {
554
                if (d1 == null) {
1✔
555
                        return d2 == null;
1✔
556
                } else if (d2 == null) {
1✔
557
                        return false;
1✔
558
                }
559
                return (d1 instanceof Date && d2 instanceof Date) ? compare((Date) d1, (Date) d2) == 0 : d1.equals(d2);
1✔
560
        }
561
        
562
        /**
563
         * Compares two java.util.Date objects, but handles java.sql.Timestamp (which is not directly
564
         * comparable to a date) by dropping its nanosecond value.
565
         */
566
        public static int compare(Date d1, Date d2) {
567
                if (d1 instanceof Timestamp && d2 instanceof Timestamp) {
1✔
568
                        return d1.compareTo(d2);
1✔
569
                }
570
                if (d1 instanceof Timestamp) {
1✔
571
                        d1 = new Date(d1.getTime());
1✔
572
                }
573
                if (d2 instanceof Timestamp) {
1✔
574
                        d2 = new Date(d2.getTime());
1✔
575
                }
576
                return d1.compareTo(d2);
1✔
577
        }
578
        
579
        /**
580
         * Compares two Date/Timestamp objects, treating null as the earliest possible date.
581
         */
582
        public static int compareWithNullAsEarliest(Date d1, Date d2) {
583
                if (d1 == null && d2 == null) {
1✔
584
                        return 0;
×
585
                }
586
                if (d1 == null) {
1✔
587
                        return -1;
1✔
588
                } else if (d2 == null) {
1✔
589
                        return 1;
1✔
590
                } else {
591
                        return compare(d1, d2);
1✔
592
                }
593
        }
594
        
595
        /**
596
         * Compares two Date/Timestamp objects, treating null as the earliest possible date.
597
         */
598
        public static int compareWithNullAsLatest(Date d1, Date d2) {
599
                if (d1 == null && d2 == null) {
1✔
600
                        return 0;
1✔
601
                }
602
                if (d1 == null) {
1✔
603
                        return 1;
1✔
604
                } else if (d2 == null) {
1✔
605
                        return -1;
1✔
606
                } else {
607
                        return compare(d1, d2);
1✔
608
                }
609
        }
610
        
611
        public static <E extends Comparable<E>> int compareWithNullAsLowest(E c1, E c2) {
612
                if (c1 == null && c2 == null) {
1✔
613
                        return 0;
×
614
                }
615
                if (c1 == null) {
1✔
616
                        return -1;
1✔
617
                } else if (c2 == null) {
1✔
618
                        return 1;
×
619
                } else {
620
                        return c1.compareTo(c2);
1✔
621
                }
622
        }
623
        
624
        public static <E extends Comparable<E>> int compareWithNullAsGreatest(E c1, E c2) {
625
                if (c1 == null && c2 == null) {
1✔
626
                        return 0;
1✔
627
                }
628
                if (c1 == null) {
1✔
629
                        return 1;
1✔
630
                } else if (c2 == null) {
1✔
631
                        return -1;
×
632
                } else {
633
                        return c1.compareTo(c2);
1✔
634
                }
635
        }
636
        
637
        /**
638
         * Converts a collection to a String with a specified separator between all elements
639
         * 
640
         * @param c Collection to be joined
641
         * @param separator string to put between all elements
642
         * @return a String representing the toString() of all elements in c, separated by separator
643
         * @deprecated as of 2.2 use Java's {@link String#join} or Apache Commons StringUtils.join for iterables which do not extend {@link CharSequence}
644
         */
645
        @Deprecated
646
        public static <E> String join(Collection<E> c, String separator) {
647
                if (c == null) {
1✔
648
                        return "";
1✔
649
                }
650
                
651
                StringBuilder ret = new StringBuilder();
1✔
652
                for (Iterator<E> i = c.iterator(); i.hasNext();) {
1✔
653
                        ret.append(i.next());
1✔
654
                        if (i.hasNext()) {
1✔
655
                                ret.append(separator);
1✔
656
                        }
657
                }
658
                return ret.toString();
1✔
659
        }
660
        
661
        public static Set<Concept> conceptSetHelper(String descriptor) {
662
                Set<Concept> ret = new HashSet<>();
×
663
                if (descriptor == null || descriptor.length() == 0) {
×
664
                        return ret;
×
665
                }
666
                ConceptService cs = Context.getConceptService();
×
667
                
668
                for (StringTokenizer st = new StringTokenizer(descriptor, "|"); st.hasMoreTokens();) {
×
669
                        String s = st.nextToken().trim();
×
670
                        boolean isSet = s.startsWith("set:");
×
671
                        if (isSet) {
×
672
                                s = s.substring(4).trim();
×
673
                        }
674
                        Concept c = null;
×
675
                        if (s.startsWith("name:")) {
×
676
                                String name = s.substring(5).trim();
×
677
                                c = cs.getConceptByName(name);
×
678
                        } else {
×
679
                                try {
680
                                        c = cs.getConcept(Integer.valueOf(s.trim()));
×
681
                                }
682
                                catch (Exception ex) {}
×
683
                        }
684
                        if (c != null) {
×
685
                                if (isSet) {
×
686
                                        List<Concept> inSet = cs.getConceptsByConceptSet(c);
×
687
                                        ret.addAll(inSet);
×
688
                                } else {
×
689
                                        ret.add(c);
×
690
                                }
691
                        }
692
                }
×
693
                return ret;
×
694
        }
695
        
696
        /**
697
         * Parses and loads a delimited list of concept ids or names
698
         * 
699
         * @param delimitedString the delimited list of concept ids or names
700
         * @param delimiter the delimiter, e.g. ","
701
         * @return the list of concepts
702
         * @since 1.10, 1.9.2, 1.8.5
703
         */
704
        public static List<Concept> delimitedStringToConceptList(String delimitedString, String delimiter) {
705
                List<Concept> ret = null;
×
706
                
707
                if (delimitedString != null) {
×
708
                        String[] tokens = delimitedString.split(delimiter);
×
709
                        for (String token : tokens) {
×
710
                                Integer conceptId;
711
                                
712
                                try {
713
                                        conceptId = Integer.valueOf(token);
×
714
                                }
715
                                catch (NumberFormatException nfe) {
×
716
                                        conceptId = null;
×
717
                                }
×
718
                                
719
                                Concept c;
720
                                
721
                                if (conceptId != null) {
×
722
                                        c = Context.getConceptService().getConcept(conceptId);
×
723
                                } else {
724
                                        c = Context.getConceptService().getConceptByName(token);
×
725
                                }
726
                                
727
                                if (c != null) {
×
728
                                        if (ret == null) {
×
729
                                                ret = new ArrayList<>();
×
730
                                        }
731
                                        ret.add(c);
×
732
                                }
733
                        }
734
                }
735
                
736
                return ret;
×
737
        }
738
        
739
        public static Map<String, Concept> delimitedStringToConceptMap(String delimitedString, String delimiter) {
740
                Map<String, Concept> ret = null;
×
741
                
742
                if (delimitedString != null) {
×
743
                        String[] tokens = delimitedString.split(delimiter);
×
744
                        for (String token : tokens) {
×
745
                                Concept c = Context.getConceptService().getConcept(token);
×
746
                                
747
                                if (c != null) {
×
748
                                        if (ret == null) {
×
749
                                                ret = new HashMap<>();
×
750
                                        }
751
                                        ret.put(token, c);
×
752
                                }
753
                        }
754
                }
755
                
756
                return ret;
×
757
        }
758

759
        public static List<Concept> conceptListHelper(String descriptor) {
760
                Set<Concept> ret = new LinkedHashSet<>();
1✔
761
                if (descriptor == null || descriptor.length() == 0) {
1✔
762
                        return Collections.emptyList();
×
763
                }
764
                ConceptService cs = Context.getConceptService();
1✔
765
                
766
                for (StringTokenizer st = new StringTokenizer(descriptor, "|"); st.hasMoreTokens();) {
1✔
767
                        String s = st.nextToken().trim();
1✔
768
                        boolean isSet = s.startsWith("set:");
1✔
769
                        if (isSet) {
1✔
770
                                s = s.substring(4).trim();
1✔
771
                        }
772
                        Concept c = null;
1✔
773
                        if (s.startsWith("name:")) {
1✔
774
                                String name = s.substring(5).trim();
1✔
775
                                c = cs.getConceptByName(name);
1✔
776
                        } else {
1✔
777
                                try {
778
                                        c = cs.getConcept(Integer.valueOf(s.trim()));
1✔
779
                                }
780
                                catch (Exception ex) {}
1✔
781
                        }
782
                        if (c != null) {
1✔
783
                                if (isSet) {
1✔
784
                                        List<Concept> inSet = cs.getConceptsByConceptSet(c);
1✔
785
                                        ret.addAll(inSet);
1✔
786
                                } else {
1✔
787
                                        ret.add(c);
1✔
788
                                }
789
                        }
790
                }
1✔
791
                return new ArrayList<>(ret);
1✔
792
        }
793
        
794
        /**
795
         * Gets the date having the last millisecond of a given day. Meaning that the hours, seconds,
796
         * and milliseconds are the latest possible for that day.
797
         * 
798
         * @param day the day.
799
         * @return the date with the last millisecond of the day.
800
         */
801
        public static Date getLastMomentOfDay(Date day) {
802
                Calendar calender = Calendar.getInstance();
1✔
803
                calender.setTime(day);
1✔
804
                calender.set(Calendar.HOUR_OF_DAY, 23);
1✔
805
                calender.set(Calendar.MINUTE, 59);
1✔
806
                calender.set(Calendar.SECOND, 59);
1✔
807
                calender.set(Calendar.MILLISECOND, 999);
1✔
808
                
809
                return calender.getTime();
1✔
810
        }
811
        
812
        /**
813
         * Return a date that is the same day as the passed in date, but the hours and seconds are the
814
         * earliest possible for that day.
815
         * 
816
         * @param date date to adjust
817
         * @return a date that is the first possible time in the day
818
         * @since 1.9
819
         */
820
        public static Date firstSecondOfDay(Date date) {
821
                if (date == null) {
1✔
822
                        return null;
×
823
                }
824
                
825
                Calendar c = Calendar.getInstance();
1✔
826
                c.setTime(date);
1✔
827
                c.set(Calendar.HOUR_OF_DAY, 0);
1✔
828
                c.set(Calendar.MINUTE, 0);
1✔
829
                c.set(Calendar.SECOND, 0);
1✔
830
                c.set(Calendar.MILLISECOND, 0);
1✔
831
                
832
                return c.getTime();
1✔
833
        }
834
        
835
        public static Date safeDate(Date d1) {
836
                return new Date(d1.getTime());
×
837
        }
838
        
839
        /**
840
         * Recursively deletes files in the given <code>dir</code> folder
841
         * 
842
         * @param dir File directory to delete
843
         * @return true/false whether the delete was completed successfully
844
         * @throws IOException if <code>dir</code> is not a directory
845
         */
846
        public static boolean deleteDirectory(File dir) throws IOException {
UNCOV
847
                if (!dir.exists() || !dir.isDirectory()) {
×
848
                        throw new IOException("Could not delete directory '" + dir.getAbsolutePath() + "' (not a directory)");
×
849
                }
850
                
UNCOV
851
                log.debug("Deleting directory {}", dir.getAbsolutePath());
×
852
                
UNCOV
853
                File[] fileList = dir.listFiles();
×
UNCOV
854
                if (fileList == null) {
×
855
                        return false;
×
856
                }
UNCOV
857
                for (File f : fileList) {
×
UNCOV
858
                        if (f.isDirectory()) {
×
UNCOV
859
                                deleteDirectory(f);
×
860
                        }
UNCOV
861
                        boolean success = f.delete();
×
862
                        
UNCOV
863
                        if (log.isDebugEnabled()) {
×
864
                                log.debug("   deleting " + f.getName() + " : " + (success ? "ok" : "failed"));
×
865
                        }
866
                        
UNCOV
867
                        if (!success) {
×
UNCOV
868
                                f.deleteOnExit();
×
869
                        }
870
                }
871
                
UNCOV
872
                boolean success = dir.delete();
×
873
                
UNCOV
874
                if (!success) {
×
875
                        log.warn("   ...could not remove directory: " + dir.getAbsolutePath());
×
876
                        dir.deleteOnExit();
×
877
                }
878
                
UNCOV
879
                if (success && log.isDebugEnabled()) {
×
880
                        log.debug("   ...and directory itself");
×
881
                }
882
                
UNCOV
883
                return success;
×
884
        }
885
        
886
        /**
887
         * Utility method to convert local URL to a File object.
888
         * 
889
         * @param url an URL
890
         * @return file object for given URL or <code>null</code> if URL is not local
891
         * <strong>Should</strong> return null given null parameter
892
         */
893
        public static File url2file(final URL url) {
894
                if (url == null || !"file".equalsIgnoreCase(url.getProtocol())) {
1✔
895
                        return null;
1✔
896
                }
897
                return new File(url.getFile().replaceAll("%20", " "));
1✔
898
        }
899
        
900
        /**
901
         * Opens input stream for given resource. This method behaves differently for different URL
902
         * types:
903
         * <ul>
904
         * <li>for <b>local files</b> it returns buffered file input stream;</li>
905
         * <li>for <b>local JAR files</b> it reads resource content into memory buffer and returns byte
906
         * array input stream that wraps those buffer (this prevents locking JAR file);</li>
907
         * <li>for <b>common URL's</b> this method simply opens stream to that URL using standard URL
908
         * API.</li>
909
         * </ul>
910
         * It is not recommended to use this method for big resources within JAR files.
911
         * 
912
         * @param url resource URL
913
         * @return input stream for given resource
914
         * @throws IOException if any I/O error has occurred
915
         */
916
        public static InputStream getResourceInputStream(final URL url) throws IOException {
917
                File file = url2file(url);
×
918
                if (file != null) {
×
919
                        return new BufferedInputStream(new FileInputStream(file));
×
920
                }
921
                if (!"jar".equalsIgnoreCase(url.getProtocol())) {
×
922
                        return url.openStream();
×
923
                }
924
                String urlStr = url.toExternalForm();
×
925
                if (urlStr.endsWith("!/")) {
×
926
                        // JAR URL points to a root entry
927
                        throw new FileNotFoundException(url.toExternalForm());
×
928
                }
929
                int p = urlStr.indexOf("!/");
×
930
                if (p == -1) {
×
931
                        throw new MalformedURLException(url.toExternalForm());
×
932
                }
933
                String path = urlStr.substring(p + 2);
×
934
                file = url2file(new URL(urlStr.substring(4, p)));
×
935
                if (file == null) {// non-local JAR file URL
×
936
                        return url.openStream();
×
937
                }
938
                try (JarFile jarFile = new JarFile(file)) {
×
939
                        ZipEntry entry = jarFile.getEntry(path);
×
940
                        if (entry == null) {
×
941
                                throw new FileNotFoundException(url.toExternalForm());
×
942
                        }
943
                        try (InputStream in = jarFile.getInputStream(entry)) {
×
944
                                ByteArrayOutputStream out = new ByteArrayOutputStream();
×
945
                                copyFile(in, out);
×
946
                                return new ByteArrayInputStream(out.toByteArray());
×
947
                        }
948
                }
949
        }
950
        
951
        /**
952
         * <pre>
953
         * Returns the application data directory. Searches for the value first 
954
         * in the "OPENMRS_APPLICATION_DATA_DIRECTORY" system property and "application_data_directory" runtime property, then in the servlet
955
         * init parameter "application.data.directory." If not found, returns:
956
         * a) "{user.home}/.OpenMRS" on UNIX-based systems
957
         * b) "{user.home}\Application Data\OpenMRS" on Windows
958
         * 
959
         * </pre>
960
         * 
961
         * @return The path to the directory on the file system that will hold miscellaneous data about
962
         *         the application (runtime properties, modules, etc)
963
         */
964
        public static String getApplicationDataDirectory() {
965
                return getApplicationDataDirectoryAsFile().toString();
1✔
966
        }
967
        
968
        public static File getApplicationDataDirectoryAsFile() {
969
                String filepath = null;
1✔
970
                final String openmrsDir = "OpenMRS";
1✔
971
                
972
                String systemProperty = System.getProperty(OpenmrsConstants.KEY_OPENMRS_APPLICATION_DATA_DIRECTORY);
1✔
973
                //System and runtime property take precedence
974
                if (StringUtils.isNotBlank(systemProperty)) {
1✔
975
                        filepath = systemProperty;
1✔
976
                } else {
977
                        String runtimeProperty = Context.getRuntimeProperties()
1✔
978
                                .getProperty(OpenmrsConstants.APPLICATION_DATA_DIRECTORY_RUNTIME_PROPERTY, null);
1✔
979
                        if (StringUtils.isNotBlank(runtimeProperty)) {
1✔
980
                                filepath = runtimeProperty;
×
981
                        }
982
                }
983
                
984
                if (filepath == null) {
1✔
985
                        if (OpenmrsConstants.UNIX_BASED_OPERATING_SYSTEM) {
1✔
986
                                filepath = Paths.get(System.getProperty("user.home"), "." + openmrsDir).toString();
1✔
987
                                if (!canWrite(new File(filepath))) {
1✔
988
                                        log.warn("Unable to write to users home dir, fallback to: "
×
989
                                                + OpenmrsConstants.APPLICATION_DATA_DIRECTORY_FALLBACK_UNIX);
990
                                        filepath = Paths.get(OpenmrsConstants.APPLICATION_DATA_DIRECTORY_FALLBACK_UNIX, openmrsDir).toString();
×
991
                                }
992
                        } else {
993
                                filepath = Paths.get(System.getProperty("user.home"), "Application Data", "OpenMRS").toString();
×
994
                                if (!new File(filepath).exists()) {
×
995
                                        filepath = Paths.get(System.getenv("appdata"), "OpenMRS").toString();
×
996
                                }
997
                                if (!canWrite(new File(filepath))) {
×
998
                                        log.warn("Unable to write to users home dir, fallback to: "
×
999
                                                + OpenmrsConstants.APPLICATION_DATA_DIRECTORY_FALLBACK_WIN);
1000
                                        filepath = OpenmrsConstants.APPLICATION_DATA_DIRECTORY_FALLBACK_WIN + File.separator + openmrsDir;
×
1001
                                }
1002
                        }
1003
                        
1004
                        filepath = filepath + File.separator;
1✔
1005
                }
1006
                
1007
                File folder = new File(filepath);
1✔
1008
                if (!folder.exists()) {
1✔
1009
                        folder.mkdirs();
×
1010
                }
1011
                
1012
                return folder;
1✔
1013
        }
1014
        
1015
        /**
1016
         * Can be used to override default application data directory.
1017
         * <p>
1018
         * Note that it will not override application data directory provided as a system property.
1019
         * 
1020
         * @param path
1021
         * @since 1.11
1022
         */
1023
        public static void setApplicationDataDirectory(String path) {
1024
                if (StringUtils.isBlank(path)) {
1✔
1025
                        System.clearProperty(OpenmrsConstants.KEY_OPENMRS_APPLICATION_DATA_DIRECTORY);
×
1026
                } else {
1027
                        System.setProperty(OpenmrsConstants.KEY_OPENMRS_APPLICATION_DATA_DIRECTORY, path);
1✔
1028
                }
1029
        }
1✔
1030
        
1031
        /**
1032
         * Checks if we can write to a given folder.
1033
         * 
1034
         * @param folder the directory to check.
1035
         * @return true if we can write to it, else false.
1036
         */
1037
        private static boolean canWrite(File folder) {
1038
                try {
1039
                        //We need to first create the folder if it does not exist, 
1040
                        //else File.canWrite() will return false even when we
1041
                        //have the necessary permissions.
1042
                        if (!folder.exists()) {
1✔
1043
                                folder.mkdirs();
1✔
1044
                        }
1045
                        
1046
                        return folder.canWrite();
1✔
1047
                }
1048
                catch (SecurityException ex) {
×
1049
                        //all we wanted to know is whether we have permissions
1050
                }
1051
                
1052
                return false;
×
1053
        }
1054
        
1055
        /**
1056
         * Returns the location of the OpenMRS log file.
1057
         * <p/>
1058
         * <strong>Warning:</strong> as of 2.4.4, 2.5.1, and 2.6.0 which allows configuration via a configuration file, the
1059
         * result of this call can return null if either the file appender uses a name other than
1060
         * {@link OpenmrsConstants#LOG_OPENMRS_FILE_APPENDER} or if the appender with that name is not one of the default file
1061
         * appending types.
1062
         * 
1063
         * @return the path to the OpenMRS log file
1064
         * @since 1.9.2
1065
         * @deprecated As of 2.4.4, 2.5.1, and 2.6.0; replaced by {@link OpenmrsLoggingUtil#getOpenmrsLogLocation()}
1066
         */
1067
        @Deprecated
1068
        public static String getOpenmrsLogLocation() {
1069
                return OpenmrsLoggingUtil.getOpenmrsLogLocation();
×
1070
        }
1071
        
1072
        /**
1073
         * Checks whether the current JVM version is at least Java 8.
1074
         * 
1075
         * @throws APIException if the current JVM version is earlier than Java 8
1076
         */
1077
        public static void validateJavaVersion() {
1078
                // check whether the current JVM version is at least Java 8
1079
                if (System.getProperty("java.version").matches("1\\.[0-7]\\.(.*)")) {
1✔
1080
                        throw new APIException(
×
1081
                                "OpenMRS " + OpenmrsConstants.OPENMRS_VERSION_SHORT + " requires Java 8 and above, but is running under " + 
1082
                                        System.getProperty("java.version"));
×
1083
                }
1084
        }
1✔
1085
        
1086
        /**
1087
         * Find the given folderName in the application data directory. Or, treat folderName like an
1088
         * absolute url to a directory
1089
         * 
1090
         * @param folderName
1091
         * @return folder capable of storing information
1092
         */
1093
        public static File getDirectoryInApplicationDataDirectory(String folderName) throws APIException {
1094
                // try to load the repository folder straight away.
1095
                File folder = new File(folderName);
1✔
1096
                
1097
                // if the property wasn't a full path already, assume it was intended to
1098
                // be a folder in the
1099
                // application directory
1100
                if (!folder.isAbsolute()) {
1✔
1101
                        folder = new File(getApplicationDataDirectoryAsFile(), folderName);
1✔
1102
                }
1103
                
1104
                // now create the directory folder if it doesn't exist
1105
                if (!folder.exists()) {
1✔
1106
                        log.warn("'" + folder.getAbsolutePath() + "' doesn't exist.  Creating directories now.");
1✔
1107
                        folder.mkdirs();
1✔
1108
                }
1109
                
1110
                if (!folder.isDirectory()) {
1✔
1111
                        throw new APIException("should.be.directory", new Object[] { folder.getAbsolutePath() });
×
1112
                }
1113
                
1114
                return folder;
1✔
1115
        }
1116
        
1117
        /**
1118
         * Save the given xml document to the given outfile
1119
         * 
1120
         * @param doc Document to be saved
1121
         * @param outFile file pointer to the location the xml file is to be saved to
1122
         */
1123
        public static void saveDocument(Document doc, File outFile) {
1124
                OutputStream outStream = null;
×
1125
                try {
1126
                        outStream = new FileOutputStream(outFile);
×
1127
                        TransformerFactory tFactory = TransformerFactory.newInstance();
×
1128
                        Transformer transformer = tFactory.newTransformer();
×
1129
                        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
×
1130
                        
1131
                        DocumentType doctype = doc.getDoctype();
×
1132
                        if (doctype != null) {
×
1133
                                transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, doctype.getPublicId());
×
1134
                                transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doctype.getSystemId());
×
1135
                        }
1136
                        
1137
                        DOMSource source = new DOMSource(doc);
×
1138
                        StreamResult result = new StreamResult(outStream);
×
1139
                        transformer.transform(source, result);
×
1140
                }
1141
                catch (TransformerException e) {
×
1142
                        throw new ModuleException("Error while saving dwrmodulexml back to dwr-modules.xml", e);
×
1143
                }
1144
                catch (FileNotFoundException e) {
×
1145
                        throw new ModuleException(outFile.getAbsolutePath() + " file doesn't exist.", e);
×
1146
                }
1147
                finally {
1148
                        try {
1149
                                if (outStream != null) {
×
1150
                                        outStream.close();
×
1151
                                }
1152
                        }
1153
                        catch (Exception e) {
×
1154
                                log.warn("Unable to close outstream", e);
×
1155
                        }
×
1156
                }
1157
        }
×
1158
        
1159
        public static List<Integer> delimitedStringToIntegerList(String delimitedString, String delimiter) {
1160
                List<Integer> ret = new ArrayList<>();
×
1161
                String[] tokens = delimitedString.split(delimiter);
×
1162
                for (String token : tokens) {
×
1163
                        token = token.trim();
×
1164
                        if (token.length() != 0) {
×
1165
                                ret.add(Integer.valueOf(token));
×
1166
                        }
1167
                }
1168
                return ret;
×
1169
        }
1170
        
1171
        /**
1172
         * Tests if the given String starts with any of the specified prefixes
1173
         * 
1174
         * @param str the string to test
1175
         * @param prefixes an array of prefixes to test against
1176
         * @return true if the String starts with any of the specified prefixes, otherwise false.
1177
         */
1178
        public static boolean stringStartsWith(String str, String[] prefixes) {
1179
                for (String prefix : prefixes) {
1✔
1180
                        if (StringUtils.startsWith(str, prefix)) {
1✔
1181
                                return true;
1✔
1182
                        }
1183
                }
1184
                
1185
                return false;
1✔
1186
        }
1187
        
1188
        public static boolean isConceptInList(Concept concept, List<Concept> list) {
1189
                boolean ret = false;
×
1190
                if (concept != null && list != null) {
×
1191
                        for (Concept c : list) {
×
1192
                                if (c.equals(concept)) {
×
1193
                                        ret = true;
×
1194
                                        break;
×
1195
                                }
1196
                        }
×
1197
                }
1198
                
1199
                return ret;
×
1200
        }
1201
        
1202
        public static Date fromDateHelper(Date comparisonDate, Integer withinLastDays, Integer withinLastMonths,
1203
                Integer untilDaysAgo, Integer untilMonthsAgo, Date sinceDate, Date untilDate) {
1204
                
1205
                Date ret = null;
×
1206
                if (withinLastDays != null || withinLastMonths != null) {
×
1207
                        Calendar gc = Calendar.getInstance();
×
1208
                        gc.setTime(comparisonDate != null ? comparisonDate : new Date());
×
1209
                        if (withinLastDays != null) {
×
1210
                                gc.add(Calendar.DAY_OF_MONTH, -withinLastDays);
×
1211
                        }
1212
                        if (withinLastMonths != null) {
×
1213
                                gc.add(Calendar.MONTH, -withinLastMonths);
×
1214
                        }
1215
                        ret = gc.getTime();
×
1216
                }
1217
                if (sinceDate != null && (ret == null || sinceDate.after(ret))) {
×
1218
                        ret = sinceDate;
×
1219
                }
1220
                return ret;
×
1221
        }
1222
        
1223
        public static Date toDateHelper(Date comparisonDate, Integer withinLastDays, Integer withinLastMonths,
1224
                Integer untilDaysAgo, Integer untilMonthsAgo, Date sinceDate, Date untilDate) {
1225
                
1226
                Date ret = null;
×
1227
                if (untilDaysAgo != null || untilMonthsAgo != null) {
×
1228
                        Calendar gc = Calendar.getInstance();
×
1229
                        gc.setTime(comparisonDate != null ? comparisonDate : new Date());
×
1230
                        if (untilDaysAgo != null) {
×
1231
                                gc.add(Calendar.DAY_OF_MONTH, -untilDaysAgo);
×
1232
                        }
1233
                        if (untilMonthsAgo != null) {
×
1234
                                gc.add(Calendar.MONTH, -untilMonthsAgo);
×
1235
                        }
1236
                        ret = gc.getTime();
×
1237
                }
1238
                if (untilDate != null && (ret == null || untilDate.before(ret))) {
×
1239
                        ret = untilDate;
×
1240
                }
1241
                return ret;
×
1242
        }
1243
        
1244
        /**
1245
         * @param collection
1246
         * @param elements
1247
         * @return Whether _collection_ contains any of _elements_
1248
         */
1249
        public static <T> boolean containsAny(Collection<T> collection, Collection<T> elements) {
1250
                for (T obj : elements) {
×
1251
                        if (collection.contains(obj)) {
×
1252
                                return true;
×
1253
                        }
1254
                }
×
1255
                return false;
×
1256
        }
1257
        
1258
        /**
1259
         * Allows easy manipulation of a Map&lt;?, Set&gt;
1260
         */
1261
        public static <K, V> void addToSetMap(Map<K, Set<V>> map, K key, V obj) {
1262
                Set<V> set = map.computeIfAbsent(key, k -> new HashSet<>());
×
1263
                set.add(obj);
×
1264
        }
×
1265
        
1266
        public static <K, V> void addToListMap(Map<K, List<V>> map, K key, V obj) {
1267
                List<V> list = map.computeIfAbsent(key, k -> new ArrayList<>());
×
1268
                list.add(obj);
×
1269
        }
×
1270
        
1271
        /**
1272
         * Get the current user's date format Will look similar to "mm-dd-yyyy". Depends on user's
1273
         * locale.
1274
         * 
1275
         * @return a simple date format
1276
         * <strong>Should</strong> return a pattern with four y characters in it
1277
         * <strong>Should</strong> not allow the returned SimpleDateFormat to be modified
1278
         * @since 1.5
1279
         */
1280
        public static SimpleDateFormat getDateFormat(Locale locale) {
1281
                if (dateFormatCache.containsKey(locale)) {
1✔
1282
                        return (SimpleDateFormat) dateFormatCache.get(locale).clone();
1✔
1283
                }
1284
                
1285
                // note that we are using the custom OpenmrsDateFormat class here which prevents erroneous parsing of 2-digit years
1286
                SimpleDateFormat sdf = new OpenmrsDateFormat((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, locale),
1✔
1287
                        locale);
1288
                String pattern = sdf.toPattern();
1✔
1289
                
1290
                if (!pattern.contains("yyyy")) {
1✔
1291
                        // otherwise, change the pattern to be a four digit year
1292
                        String regex = "yy";
1✔
1293
                        if (!pattern.contains("yy")) {
1✔
1294
                                //Java 11 has dd/MM/y instead of dd/MM/yy
1295
                                regex = "y";
×
1296
                        }
1297
                        pattern = pattern.replaceFirst(regex, "yyyy");
1✔
1298
                        sdf.applyPattern(pattern);
1✔
1299
                }
1300
                if (!pattern.contains("MM")) {
1✔
1301
                        // change the pattern to be a two digit month
1302
                        pattern = pattern.replaceFirst("M", "MM");
1✔
1303
                        sdf.applyPattern(pattern);
1✔
1304
                }
1305
                if (!pattern.contains("dd")) {
1✔
1306
                        // change the pattern to be a two digit day
1307
                        pattern = pattern.replaceFirst("d", "dd");
1✔
1308
                        sdf.applyPattern(pattern);
1✔
1309
                }
1310
                
1311
                dateFormatCache.put(locale, sdf);
1✔
1312
                
1313
                return (SimpleDateFormat) sdf.clone();
1✔
1314
        }
1315
        
1316
        /**
1317
         * Get the current user's time format Will look similar to "hh:mm a". Depends on user's locale.
1318
         * 
1319
         * @return a simple time format
1320
         * <strong>Should</strong> return a pattern with two h characters in it
1321
         * <strong>Should</strong> not allow the returned SimpleDateFormat to be modified
1322
         * @since 1.9
1323
         */
1324
        public static SimpleDateFormat getTimeFormat(Locale locale) {
1325
                if (timeFormatCache.containsKey(locale)) {
1✔
1326
                        return (SimpleDateFormat) timeFormatCache.get(locale).clone();
1✔
1327
                }
1328
                
1329
                SimpleDateFormat sdf = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.SHORT, locale);
1✔
1330
                String pattern = sdf.toPattern();
1✔
1331
                
1332
                if (!(pattern.contains("hh") || pattern.contains("HH"))) {
1✔
1333
                        // otherwise, change the pattern to be a two digit hour
1334
                        pattern = pattern.replaceFirst("h", "hh").replaceFirst("H", "HH");
×
1335
                        sdf.applyPattern(pattern);
×
1336
                }
1337
                
1338
                timeFormatCache.put(locale, sdf);
1✔
1339
                
1340
                return (SimpleDateFormat) sdf.clone();
1✔
1341
        }
1342
        
1343
        /**
1344
         * Get the current user's datetime format Will look similar to "mm-dd-yyyy hh:mm a". Depends on
1345
         * user's locale.
1346
         * 
1347
         * @return a simple date format
1348
         * <strong>Should</strong> return a pattern with four y characters and two h characters in it
1349
         * <strong>Should</strong> not allow the returned SimpleDateFormat to be modified
1350
         * @since 1.9
1351
         */
1352
        public static SimpleDateFormat getDateTimeFormat(Locale locale) {
1353
                SimpleDateFormat dateFormat;
1354
                SimpleDateFormat timeFormat;
1355
                
1356
                dateFormat = getDateFormat(locale);
1✔
1357
                timeFormat = getTimeFormat(locale);
1✔
1358
                
1359
                String pattern = dateFormat.toPattern() + " " + timeFormat.toPattern();
1✔
1360
                SimpleDateFormat sdf = new SimpleDateFormat();
1✔
1361
                sdf.applyPattern(pattern);
1✔
1362
                return sdf;
1✔
1363
        }
1364
        
1365
        /**
1366
         * Takes a String (e.g. a user-entered one) and parses it into an object of the specified class
1367
         * 
1368
         * @param string
1369
         * @param clazz
1370
         * @return Object of type <code>clazz</code> with the data from <code>string</code>
1371
         */
1372
        @SuppressWarnings("unchecked")
1373
        public static Object parse(String string, Class clazz) {
1374
                try {
1375
                        // If there's a valueOf(String) method, just use that (will cover at
1376
                        // least String, Integer, Double, Boolean)
1377
                        Method valueOfMethod = null;
×
1378
                        try {
1379
                                valueOfMethod = clazz.getMethod("valueOf", String.class);
×
1380
                        }
1381
                        catch (NoSuchMethodException ex) {}
×
1382
                        if (valueOfMethod != null) {
×
1383
                                return valueOfMethod.invoke(null, string);
×
1384
                        } else if (clazz.isEnum()) {
×
1385
                                // Special-case for enum types
1386
                                List<Enum> constants = Arrays.asList((Enum[]) clazz.getEnumConstants());
×
1387
                                for (Enum e : constants) {
×
1388
                                        if (e.toString().equals(string)) {
×
1389
                                                return e;
×
1390
                                        }
1391
                                }
×
1392
                                throw new IllegalArgumentException(string + " is not a legal value of enum class " + clazz);
×
1393
                        } else if (String.class.equals(clazz)) {
×
1394
                                return string;
×
1395
                        } else if (Location.class.equals(clazz)) {
×
1396
                                try {
1397
                                        Integer.parseInt(string);
×
1398
                                        LocationEditor ed = new LocationEditor();
×
1399
                                        ed.setAsText(string);
×
1400
                                        return ed.getValue();
×
1401
                                }
1402
                                catch (NumberFormatException ex) {
×
1403
                                        return Context.getLocationService().getLocation(string);
×
1404
                                }
1405
                        } else if (Concept.class.equals(clazz)) {
×
1406
                                ConceptEditor ed = new ConceptEditor();
×
1407
                                ed.setAsText(string);
×
1408
                                return ed.getValue();
×
1409
                        } else if (Program.class.equals(clazz)) {
×
1410
                                ProgramEditor ed = new ProgramEditor();
×
1411
                                ed.setAsText(string);
×
1412
                                return ed.getValue();
×
1413
                        } else if (ProgramWorkflowState.class.equals(clazz)) {
×
1414
                                ProgramWorkflowStateEditor ed = new ProgramWorkflowStateEditor();
×
1415
                                ed.setAsText(string);
×
1416
                                return ed.getValue();
×
1417
                        } else if (EncounterType.class.equals(clazz)) {
×
1418
                                EncounterTypeEditor ed = new EncounterTypeEditor();
×
1419
                                ed.setAsText(string);
×
1420
                                return ed.getValue();
×
1421
                        } else if (Form.class.equals(clazz)) {
×
1422
                                FormEditor ed = new FormEditor();
×
1423
                                ed.setAsText(string);
×
1424
                                return ed.getValue();
×
1425
                        } else if (Drug.class.equals(clazz)) {
×
1426
                                DrugEditor ed = new DrugEditor();
×
1427
                                ed.setAsText(string);
×
1428
                                return ed.getValue();
×
1429
                        } else if (PersonAttributeType.class.equals(clazz)) {
×
1430
                                PersonAttributeTypeEditor ed = new PersonAttributeTypeEditor();
×
1431
                                ed.setAsText(string);
×
1432
                                return ed.getValue();
×
1433
                        } else if (Cohort.class.equals(clazz)) {
×
1434
                                CohortEditor ed = new CohortEditor();
×
1435
                                ed.setAsText(string);
×
1436
                                return ed.getValue();
×
1437
                        } else if (Date.class.equals(clazz)) {
×
1438
                                // TODO: this uses the date format from the current session,
1439
                                // which could cause problems if the user changes it after
1440
                                // searching.
1441
                                CustomDateEditor ed = new CustomDateEditor(Context.getDateFormat(), true, 10);
×
1442
                                ed.setAsText(string);
×
1443
                                return ed.getValue();
×
1444
                        } else if (Object.class.equals(clazz)) {
×
1445
                                // TODO: Decide whether this is a hack. Currently setting Object
1446
                                // arguments with a String
1447
                                return string;
×
1448
                        } else {
1449
                                throw new IllegalArgumentException("Don't know how to handle class: " + clazz);
×
1450
                        }
1451
                }
1452
                catch (Exception ex) {
×
1453
                        log.error("error converting \"" + string + "\" to " + clazz, ex);
×
1454
                        throw new IllegalArgumentException(ex);
×
1455
                }
1456
        }
1457
        
1458
        /**
1459
         * Loops over the collection to check to see if the given object is in that collection. This
1460
         * method <i>only</i> uses the .equals() method for comparison. This should be used in the
1461
         * patient/person objects on their collections. Their collections are SortedSets which use the
1462
         * compareTo method for equality as well. The compareTo method is currently optimized for
1463
         * sorting, not for equality. A null <code>obj</code> will return false
1464
         * 
1465
         * @param objects collection to loop over
1466
         * @param obj Object to look for in the <code>objects</code>
1467
         * @return true/false whether the given object is found
1468
         * <strong>Should</strong> use equals method for comparison instead of compareTo given List collection
1469
         * <strong>Should</strong> use equals method for comparison instead of compareTo given SortedSet collection
1470
         */
1471
        public static boolean collectionContains(Collection<?> objects, Object obj) {
1472
                if (obj == null || objects == null) {
1✔
1473
                        return false;
×
1474
                }
1475
                
1476
                for (Object o : objects) {
1✔
1477
                        if (o != null && o.equals(obj)) {
1✔
1478
                                return true;
1✔
1479
                        }
1480
                }
1✔
1481
                
1482
                return false;
1✔
1483
        }
1484
        
1485
        /**
1486
         * Gets an out File object. If date is not provided, the current timestamp is used. If user is
1487
         * not provided, the user id is not put into the filename. Assumes dir is already created
1488
         * 
1489
         * @param dir directory to make the random filename in
1490
         * @param date optional Date object used for the name
1491
         * @param user optional User creating this file object
1492
         * @return file new file that is able to be written to
1493
         */
1494
        public static File getOutFile(File dir, Date date, User user) {
1495
                Random gen = new Random();
×
1496
                File outFile;
1497
                do {
1498
                        // format to print date in filename
1499
                        DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd-HHmm-ssSSS");
×
1500
                        
1501
                        // use current date if none provided
1502
                        if (date == null) {
×
1503
                                date = new Date();
×
1504
                        }
1505
                        
1506
                        StringBuilder filename = new StringBuilder();
×
1507
                        
1508
                        // the start of the filename is the time so we can do some sorting
1509
                        filename.append(dateFormat.format(date));
×
1510
                        
1511
                        // insert the user id if they provided it
1512
                        if (user != null) {
×
1513
                                filename.append("-");
×
1514
                                filename.append(user.getUserId());
×
1515
                                filename.append("-");
×
1516
                        }
1517
                        
1518
                        // the end of the filename is a randome number between 0 and 10000
1519
                        filename.append(gen.nextInt() * 10000);
×
1520
                        filename.append(".xml");
×
1521
                        
1522
                        outFile = new File(dir, filename.toString());
×
1523
                        
1524
                        // set to null to avoid very minimal possiblity of an infinite loop
1525
                        date = null;
×
1526
                        
1527
                } while (outFile.exists());
×
1528
                
1529
                return outFile;
×
1530
        }
1531
        
1532
        /**
1533
         * Creates a relatively acceptable unique string of the give size
1534
         * 
1535
         * @return unique string
1536
         */
1537
        public static String generateUid(Integer size) {
1538
                Random gen = new Random();
×
1539
                StringBuilder sb = new StringBuilder(size);
×
1540
                for (int i = 0; i < size; i++) {
×
1541
                        int ch = gen.nextInt() * 62;
×
1542
                        if (ch < 10) {
×
1543
                                // 0-9
1544
                                sb.append(ch);
×
1545
                        } else if (ch < 36) {
×
1546
                                // a-z
1547
                                sb.append((char) (ch - 10 + 'a'));
×
1548
                        } else {
1549
                                sb.append((char) (ch - 36 + 'A'));
×
1550
                        }
1551
                }
1552
                return sb.toString();
×
1553
        }
1554
        
1555
        /**
1556
         * Creates a uid of length 20
1557
         * 
1558
         * @see #generateUid(Integer)
1559
         */
1560
        public static String generateUid() {
1561
                return generateUid(20);
×
1562
        }
1563
        
1564
        /**
1565
         * Convenience method to replace Properties.store(), which isn't UTF-8 compliant <br>
1566
         * NOTE: In Java 6, you will be able to pass the load() and store() methods a UTF-8
1567
         * Reader/Writer object as an argument, making this method unnecessary.
1568
         * 
1569
         * @param properties
1570
         * @param file
1571
         * @param comment
1572
         */
1573
        public static void storeProperties(Properties properties, File file, String comment) {
1574
                OutputStream outStream = null;
×
1575
                try {
1576
                        outStream = new FileOutputStream(file, true);
×
1577
                        storeProperties(properties, outStream, comment);
×
1578
                }
1579
                catch (IOException ex) {
×
1580
                        log.error("Unable to create file " + file.getAbsolutePath() + " in storeProperties routine.");
×
1581
                }
1582
                finally {
1583
                        try {
1584
                                if (outStream != null) {
×
1585
                                        outStream.close();
×
1586
                                }
1587
                        }
1588
                        catch (IOException ioe) {
×
1589
                                // pass
1590
                        }
×
1591
                }
1592
        }
×
1593
        
1594
        /**
1595
         * Convenience method to replace Properties.store(), which isn't UTF-8 compliant NOTE: In Java
1596
         * 6, you will be able to pass the load() and store() methods a UTF-8 Reader/Writer object as an
1597
         * argument.
1598
         * 
1599
         * @param properties
1600
         * @param outStream
1601
         * @param comment (which appears in comments in properties file)
1602
         */
1603
        public static void storeProperties(Properties properties, OutputStream outStream, String comment) {
1604
                try {
1605
                        Charset utf8 = StandardCharsets.UTF_8;
1✔
1606
                        properties.store(new OutputStreamWriter(outStream, utf8), comment);
1✔
1607
                }
1608
                catch (FileNotFoundException fnfe) {
×
1609
                        log.error("target file not found" + fnfe);
×
1610
                }
1611
                catch (UnsupportedEncodingException ex) { // pass
×
1612
                        log.error("unsupported encoding error hit" + ex);
×
1613
                }
1614
                catch (IOException ioex) {
×
1615
                        log.error("IO exception encountered trying to append to properties file" + ioex);
×
1616
                }
1✔
1617
                
1618
        }
1✔
1619
        
1620
        /**
1621
         * This method is a replacement for Properties.load(InputStream) so that we can load in utf-8
1622
         * characters. Currently the load method expects the inputStream to point to a latin1 encoded
1623
         * file. <br>
1624
         * NOTE: In Java 6, you will be able to pass the load() and store() methods a UTF-8
1625
         * Reader/Writer object as an argument, making this method unnecessary.
1626
         * 
1627
         * @param props the properties object to write into
1628
         * @param inputStream the input stream to read from
1629
         */
1630
        public static void loadProperties(Properties props, InputStream inputStream) {
1631
                InputStreamReader reader = null;
1✔
1632
                try {
1633
                        reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
1✔
1634
                        props.load(reader);
1✔
1635
                }
1636
                catch (FileNotFoundException fnfe) {
×
1637
                        log.error("Unable to find properties file" + fnfe);
×
1638
                }
1639
                catch (UnsupportedEncodingException uee) {
×
1640
                        log.error("Unsupported encoding used in properties file" + uee);
×
1641
                }
1642
                catch (IOException ioe) {
×
1643
                        log.error("Unable to read properties from properties file" + ioe);
×
1644
                }
1645
                finally {
1646
                        try {
1647
                                if (reader != null) {
1✔
1648
                                        reader.close();
1✔
1649
                                }
1650
                        }
1651
                        catch (IOException ioe) {
×
1652
                                log.error("Unable to close properties file " + ioe);
×
1653
                        }
1✔
1654
                }
1655
        }
1✔
1656
        
1657
        /**
1658
         * Convenience method used to load properties from the given file.
1659
         * 
1660
         * @param props the properties object to be loaded into
1661
         * @param propertyFile the properties file to read
1662
         */
1663
        public static void loadProperties(Properties props, File propertyFile) {
1664
                try {
1665
                        loadProperties(props, new FileInputStream(propertyFile));
1✔
1666
                }
1667
                catch (FileNotFoundException fnfe) {
×
1668
                        log.error("Unable to find properties file" + fnfe);
×
1669
                }
1✔
1670
        }
1✔
1671
        
1672
        /**
1673
         * Utility method for getting the translation for the passed code
1674
         * 
1675
         * @param code the message key to lookup
1676
         * @param args the replacement values for the translation string
1677
         * @return the message, or if not found, the code
1678
         */
1679
        public static String getMessage(String code, Object... args) {
1680
                Locale l = Context.getLocale();
1✔
1681
                try {
1682
                        String translation = Context.getMessageSourceService().getMessage(code, args, l);
1✔
1683
                        if (translation != null) {
1✔
1684
                                return translation;
1✔
1685
                        }
1686
                }
1687
                catch (NoSuchMessageException e) {
×
1688
                        log.warn("Message code <" + code + "> not found for locale " + l);
×
1689
                }
1690
                catch (APIException apiEx) {
×
1691
                        // in case the services aren't set up yet
1692
                        log.debug("Unable to get code: " + code, apiEx);
×
1693
                        return code;
×
1694
                }
×
1695
                return code;
×
1696
        }
1697
        
1698
        /**
1699
         * Utility to check the validity of a password for a certain {@link User}. Passwords must be
1700
         * non-null. Their required strength is configured via global properties:
1701
         * <table summary="Configuration props">
1702
         * <tr>
1703
         * <th>Description</th>
1704
         * <th>Property</th>
1705
         * <th>Default Value</th>
1706
         * </tr>
1707
         * <tr>
1708
         * <th>Require that it not match the {@link User}'s username or system id
1709
         * <th>{@link OpenmrsConstants#GP_PASSWORD_CANNOT_MATCH_USERNAME_OR_SYSTEMID}</th>
1710
         * <th>true</th>
1711
         * </tr>
1712
         * <tr>
1713
         * <th>Require a minimum length
1714
         * <th>{@link OpenmrsConstants#GP_PASSWORD_MINIMUM_LENGTH}</th>
1715
         * <th>8</th>
1716
         * </tr>
1717
         * <tr>
1718
         * <th>Require both an upper and lower case character
1719
         * <th>{@link OpenmrsConstants#GP_PASSWORD_REQUIRES_UPPER_AND_LOWER_CASE}</th>
1720
         * <th>true</th>
1721
         * </tr>
1722
         * <tr>
1723
         * <th>Require at least one numeric character
1724
         * <th>{@link OpenmrsConstants#GP_PASSWORD_REQUIRES_DIGIT}</th>
1725
         * <th>true</th>
1726
         * </tr>
1727
         * <tr>
1728
         * <th>Require at least one non-numeric character
1729
         * <th>{@link OpenmrsConstants#GP_PASSWORD_REQUIRES_NON_DIGIT}</th>
1730
         * <th>true</th>
1731
         * </tr>
1732
         * <tr>
1733
         * <th>Require a match on the specified regular expression
1734
         * <th>{@link OpenmrsConstants#GP_PASSWORD_CUSTOM_REGEX}</th>
1735
         * <th>null</th>
1736
         * </tr>
1737
         * </table>
1738
         * 
1739
         * @param username user name of the user with password to validated
1740
         * @param password string that will be validated
1741
         * @param systemId system id of the user with password to be validated
1742
         * @throws PasswordException
1743
         * @since 1.5
1744
         * <strong>Should</strong> fail with short password by default
1745
         * <strong>Should</strong> fail with short password if not allowed
1746
         * <strong>Should</strong> pass with short password if allowed
1747
         * <strong>Should</strong> fail with digit only password by default
1748
         * <strong>Should</strong> fail with digit only password if not allowed
1749
         * <strong>Should</strong> pass with digit only password if allowed
1750
         * <strong>Should</strong> fail with char only password by default
1751
         * <strong>Should</strong> fail with char only password if not allowed
1752
         * <strong>Should</strong> pass with char only password if allowed
1753
         * <strong>Should</strong> fail without both upper and lower case password by default
1754
         * <strong>Should</strong> fail without both upper and lower case password if not allowed
1755
         * <strong>Should</strong> pass without both upper and lower case password if allowed
1756
         * <strong>Should</strong> fail with password equals to user name by default
1757
         * <strong>Should</strong> fail with password equals to user name if not allowed
1758
         * <strong>Should</strong> pass with password equals to user name if allowed
1759
         * <strong>Should</strong> fail with password equals to system id by default
1760
         * <strong>Should</strong> fail with password equals to system id if not allowed
1761
         * <strong>Should</strong> pass with password equals to system id if allowed
1762
         * <strong>Should</strong> fail with password not matching configured regex
1763
         * <strong>Should</strong> pass with password matching configured regex
1764
         * <strong>Should</strong> allow password to contain non alphanumeric characters
1765
         * <strong>Should</strong> allow password to contain white spaces
1766
         * <strong>Should</strong> still work without an open session
1767
         */
1768
        public static void validatePassword(String username, String password, String systemId) throws PasswordException {
1769
                
1770
                // default values for all of the global properties
1771
                String userGp = "true";
1✔
1772
                String lengthGp = "8";
1✔
1773
                String caseGp = "true";
1✔
1774
                String digitGp = "true";
1✔
1775
                String nonDigitGp = "true";
1✔
1776
                String regexGp = null;
1✔
1777
                AdministrationService svc = null;
1✔
1778
                
1779
                try {
1780
                        svc = Context.getAdministrationService();
1✔
1781
                }
1782
                catch (APIException apiEx) {
×
1783
                        // if a service isn't available, fail quietly and just do the
1784
                        // defaults
1785
                        log.debug("Unable to get global properties", apiEx);
×
1786
                }
1✔
1787
                
1788
                if (svc != null && Context.isSessionOpen()) {
1✔
1789
                        // (the session won't be open here to allow for the unit test to
1790
                        // fake not having the admin service available)
1791
                        userGp = svc.getGlobalProperty(OpenmrsConstants.GP_PASSWORD_CANNOT_MATCH_USERNAME_OR_SYSTEMID, userGp);
1✔
1792
                        lengthGp = svc.getGlobalProperty(OpenmrsConstants.GP_PASSWORD_MINIMUM_LENGTH, lengthGp);
1✔
1793
                        caseGp = svc.getGlobalProperty(OpenmrsConstants.GP_PASSWORD_REQUIRES_UPPER_AND_LOWER_CASE, caseGp);
1✔
1794
                        digitGp = svc.getGlobalProperty(OpenmrsConstants.GP_PASSWORD_REQUIRES_DIGIT, digitGp);
1✔
1795
                        nonDigitGp = svc.getGlobalProperty(OpenmrsConstants.GP_PASSWORD_REQUIRES_NON_DIGIT, nonDigitGp);
1✔
1796
                        regexGp = svc.getGlobalProperty(OpenmrsConstants.GP_PASSWORD_CUSTOM_REGEX, regexGp);
1✔
1797
                }
1798
                
1799
                if (password == null) {
1✔
1800
                        throw new WeakPasswordException();
×
1801
                }
1802
                
1803
                if ("true".equals(userGp) && (password.equals(username) || password.equals(systemId))) {
1✔
1804
                        throw new WeakPasswordException();
1✔
1805
                }
1806
                
1807
                if (StringUtils.isNotEmpty(lengthGp)) {
1✔
1808
                        try {
1809
                                int minLength = Integer.parseInt(lengthGp);
1✔
1810
                                if (password.length() < minLength) {
1✔
1811
                                        throw new ShortPasswordException(getMessage("error.password.length", lengthGp));
1✔
1812
                                }
1813
                        }
1814
                        catch (NumberFormatException nfe) {
×
1815
                                log.warn(
×
1816
                                    "Error in global property <" + OpenmrsConstants.GP_PASSWORD_MINIMUM_LENGTH + "> must be an Integer");
1817
                        }
1✔
1818
                }
1819
                
1820
                if ("true".equals(caseGp) && !containsUpperAndLowerCase(password)) {
1✔
1821
                        throw new InvalidCharactersPasswordException(getMessage("error.password.requireMixedCase"));
1✔
1822
                }
1823
                
1824
                if ("true".equals(digitGp) && !containsDigit(password)) {
1✔
1825
                        throw new InvalidCharactersPasswordException(getMessage("error.password.requireNumber"));
×
1826
                }
1827
                
1828
                if ("true".equals(nonDigitGp) && containsOnlyDigits(password)) {
1✔
1829
                        throw new InvalidCharactersPasswordException(getMessage("error.password.requireLetter"));
×
1830
                }
1831
                
1832
                if (StringUtils.isNotEmpty(regexGp)) {
1✔
1833
                        try {
1834
                                Pattern pattern = Pattern.compile(regexGp);
1✔
1835
                                Matcher matcher = pattern.matcher(password);
1✔
1836
                                if (!matcher.matches()) {
1✔
1837
                                        throw new InvalidCharactersPasswordException(getMessage("error.password.different"));
1✔
1838
                                }
1839
                        }
1840
                        catch (PatternSyntaxException pse) {
×
1841
                                log.warn("Invalid regex of " + regexGp + " defined in global property <"
×
1842
                                        + OpenmrsConstants.GP_PASSWORD_CUSTOM_REGEX + ">.");
1843
                        }
1✔
1844
                }
1845
        }
1✔
1846
        
1847
        /**
1848
         * @param test the string to test
1849
         * @return true if the passed string contains both upper and lower case characters
1850
         * <strong>Should</strong> return true if string contains upper and lower case
1851
         * <strong>Should</strong> return false if string does not contain lower case characters
1852
         * <strong>Should</strong> return false if string does not contain upper case characters
1853
         */
1854
        public static boolean containsUpperAndLowerCase(String test) {
1855
                if (test != null) {
1✔
1856
                        Pattern pattern = Pattern.compile("^(?=.*?[A-Z])(?=.*?[a-z])[\\w|\\W]*$");
1✔
1857
                        Matcher matcher = pattern.matcher(test);
1✔
1858
                        return matcher.matches();
1✔
1859
                }
1860
                return false;
1✔
1861
        }
1862
        
1863
        /**
1864
         * @param test the string to test
1865
         * @return true if the passed string contains only numeric characters
1866
         * <strong>Should</strong> return true if string contains only digits
1867
         * <strong>Should</strong> return false if string contains any non-digits
1868
         */
1869
        public static boolean containsOnlyDigits(String test) {
1870
                if (test != null) {
1✔
1871
                        for (char c : test.toCharArray()) {
1✔
1872
                                if (!Character.isDigit(c)) {
1✔
1873
                                        return false;
1✔
1874
                                }
1875
                        }
1876
                }
1877
                return StringUtils.isNotEmpty(test);
1✔
1878
        }
1879
        
1880
        /**
1881
         * @param test the string to test
1882
         * @return true if the passed string contains any numeric characters
1883
         * <strong>Should</strong> return true if string contains any digits
1884
         * <strong>Should</strong> return false if string contains no digits
1885
         */
1886
        public static boolean containsDigit(String test) {
1887
                if (test != null) {
1✔
1888
                        for (char c : test.toCharArray()) {
1✔
1889
                                if (Character.isDigit(c)) {
1✔
1890
                                        return true;
1✔
1891
                                }
1892
                        }
1893
                }
1894
                return false;
1✔
1895
        }
1896
        
1897
        /**
1898
         * A null-safe and exception safe way to close an inputstream or an outputstream
1899
         * 
1900
         * @param closableStream an InputStream or OutputStream to close
1901
         */
1902
        public static void closeStream(Closeable closableStream) {
1903
                if (closableStream != null) {
×
1904
                        try {
1905
                                closableStream.close();
×
1906
                        }
1907
                        catch (IOException io) {
×
1908
                                log.trace("Error occurred while closing stream", io);
×
1909
                        }
×
1910
                }
1911
        }
×
1912
        
1913
        /**
1914
         * Convert a stack trace into a shortened version for easier viewing and data storage, excluding
1915
         * those lines we are least concerned with; should average about 60% reduction in stack trace
1916
         * length
1917
         * 
1918
         * @param stackTrace original stack trace from an error
1919
         * @return shortened stack trace
1920
         * <strong>Should</strong> return null if stackTrace is null
1921
         * <strong>Should</strong> remove springframework and reflection related lines
1922
         * @since 1.7
1923
         */
1924
        public static String shortenedStackTrace(String stackTrace) {
1925
                if (stackTrace == null) {
1✔
1926
                        return null;
1✔
1927
                }
1928
                
1929
                List<String> results = new ArrayList<>();
1✔
1930
                final Pattern exclude = Pattern.compile("(org.springframework.|java.lang.reflect.Method.invoke|sun.reflect.)");
1✔
1931
                boolean found = false;
1✔
1932
                
1933
                for (String line : stackTrace.split("\n")) {
1✔
1934
                        Matcher m = exclude.matcher(line);
1✔
1935
                        if (m.find()) {
1✔
1936
                                found = true;
1✔
1937
                        } else {
1938
                                if (found) {
1✔
1939
                                        found = false;
1✔
1940
                                        results.add("\tat [ignored] ...");
1✔
1941
                                }
1942
                                results.add(line);
1✔
1943
                        }
1944
                }
1945
                
1946
                return StringUtils.join(results, "\n");
1✔
1947
        }
1948
        
1949
        /**
1950
         * <pre>
1951
         * Finds and loads the runtime properties file for a specific OpenMRS application.
1952
         * Searches for the file in this order:
1953
         * 1) {current directory}/{applicationname}_runtime.properties
1954
         * 2) an environment variable called "{APPLICATIONNAME}_RUNTIME_PROPERTIES_FILE"
1955
         * 3) {openmrs_app_dir}/{applicationName}_runtime.properties   // openmrs_app_dir is typically {user_home}/.OpenMRS
1956
         * </pre>
1957
         * 
1958
         * @see #getApplicationDataDirectory()
1959
         * @param applicationName (defaults to "openmrs") the name of the running OpenMRS application,
1960
         *            e.g. if you have deployed OpenMRS as a web application you would give the deployed
1961
         *            context path here
1962
         * @return runtime properties, or null if none can be found
1963
         * @since 1.8
1964
         */
1965
        public static Properties getRuntimeProperties(String applicationName) {
1966
                if (applicationName == null) {
×
1967
                        applicationName = "openmrs";
×
1968
                }
1969
                String pathName;
1970
                pathName = getRuntimePropertiesFilePathName(applicationName);
×
1971
                FileInputStream propertyStream = null;
×
1972
                try {
1973
                        if (pathName != null) {
×
1974
                                propertyStream = new FileInputStream(pathName);
×
1975
                        }
1976
                }
1977
                catch (FileNotFoundException e) {
×
1978
                        log.warn("Unable to find a runtime properties file at " + new File(pathName).getAbsolutePath());
×
1979
                }
×
1980
                
1981
                try {
1982
                        if (propertyStream == null) {
×
1983
                                throw new IOException("Could not find a runtime properties file named " + pathName
×
1984
                                        + " in the OpenMRS application data directory, or the current directory");
1985
                        }
1986
                        
1987
                        Properties props = new Properties();
×
1988
                        OpenmrsUtil.loadProperties(props, propertyStream);
×
1989
                        propertyStream.close();
×
1990
                        log.info("Using runtime properties file: " + pathName);
×
1991
                        return props;
×
1992
                }
1993
                catch (Exception ex) {
×
1994
                        log.info("Got an error while attempting to load the runtime properties", ex);
×
1995
                        log.warn(
×
1996
                            "Unable to find a runtime properties file. Initial setup is needed. View the webapp to run the setup wizard.");
1997
                        return null;
×
1998
                }
1999
        }
2000
        
2001
        /**
2002
         * Checks whether the system is running in test mode
2003
         * 
2004
         * @return boolean
2005
         */
2006
        
2007
        public static boolean isTestMode() {
2008
                return "true".equalsIgnoreCase(System.getProperty("FUNCTIONAL_TEST_MODE"));
1✔
2009
        }
2010
        
2011
        /**
2012
         * Gets the full path and name of the runtime properties file.
2013
         * 
2014
         * @param applicationName (defaults to "openmrs") the name of the running OpenMRS application,
2015
         *            e.g. if you have deployed OpenMRS as a web application you would give the deployed
2016
         *            context path here
2017
         * @return runtime properties file path and name, or null if none can be found
2018
         * @since 1.9
2019
         */
2020
        public static String getRuntimePropertiesFilePathName(String applicationName) {
2021
                if (applicationName == null) {
1✔
2022
                        applicationName = "openmrs";
×
2023
                }
2024
                
2025
                String defaultFileName = applicationName + "-runtime.properties";
1✔
2026
                String fileNameInTestMode = getRuntimePropertiesFileNameInTestMode();
1✔
2027
                
2028
                // first look in the current directory (that java was started from)
2029
                String pathName = fileNameInTestMode != null ? fileNameInTestMode : defaultFileName;
1✔
2030
                log.debug("Attempting to look for properties file in current directory: " + pathName);
1✔
2031
                if (new File(pathName).exists()) {
1✔
2032
                        return pathName;
×
2033
                } else {
2034
                        log.warn("Unable to find a runtime properties file at " + new File(pathName).getAbsolutePath());
1✔
2035
                }
2036
                
2037
                // next look from environment variable
2038
                String envVarName = applicationName.toUpperCase() + "_RUNTIME_PROPERTIES_FILE";
1✔
2039
                String envFileName = System.getenv(envVarName);
1✔
2040
                if (envFileName != null) {
1✔
2041
                        log.debug("Atempting to look for runtime properties from: " + pathName);
×
2042
                        if (new File(envFileName).exists()) {
×
2043
                                return envFileName;
×
2044
                        } else {
2045
                                log.warn("Unable to find properties file with path: " + pathName + ". (derived from environment variable "
×
2046
                                        + envVarName + ")");
2047
                        }
2048
                } else {
2049
                        log.info("Couldn't find an environment variable named " + envVarName);
1✔
2050
                        if (log.isDebugEnabled()) {
1✔
2051
                                log.debug("Available environment variables are named: " + System.getenv().keySet());
×
2052
                        }
2053
                }
2054
                
2055
                // next look in the OpenMRS application data directory
2056
                File file = new File(getApplicationDataDirectory(), pathName);
1✔
2057
                pathName = file.getAbsolutePath();
1✔
2058
                log.debug("Attempting to look for property file from: " + pathName);
1✔
2059
                if (file.exists()) {
1✔
2060
                        return pathName;
×
2061
                } else {
2062
                        log.warn("Unable to find properties file: " + pathName);
1✔
2063
                }
2064
                
2065
                return null;
1✔
2066
        }
2067
        
2068
        public static String getRuntimePropertiesFileNameInTestMode() {
2069
                String filename = null;
1✔
2070
                if (isTestMode()) {
1✔
2071
                        log.info("In functional testing mode. Ignoring the existing runtime properties file");
×
2072
                        filename = getOpenMRSVersionInTestMode() + "-test-runtime.properties";
×
2073
                }
2074
                return filename;
1✔
2075
        }
2076
        
2077
        /**
2078
         * Gets OpenMRS version name under test mode.
2079
         * 
2080
         * @return String openmrs version number
2081
         */
2082
        public static String getOpenMRSVersionInTestMode() {
2083
                return System.getProperty("OPENMRS_VERSION", "openmrs");
×
2084
        }
2085
        
2086
        /**
2087
         * Performs a case insensitive Comparison of two strings taking care of null values
2088
         * 
2089
         * @param s1 the string to compare
2090
         * @param s2 the string to compare
2091
         * @return true if strings are equal (ignoring case)
2092
         * <strong>Should</strong> return false if only one of the strings is null
2093
         * <strong>Should</strong> be case insensitive
2094
         * @since 1.8
2095
         */
2096
        public static boolean nullSafeEqualsIgnoreCase(String s1, String s2) {
2097
                if (s1 == null) {
1✔
2098
                        return s2 == null;
1✔
2099
                } else if (s2 == null) {
1✔
2100
                        return false;
1✔
2101
                }
2102
                
2103
                return s1.equalsIgnoreCase(s2);
1✔
2104
        }
2105
        
2106
        /**
2107
         * This method converts the given Long value to an Integer. If the Long value will not fit in an
2108
         * Integer an exception is thrown
2109
         * 
2110
         * @param longValue the value to convert
2111
         * @return the long value in integer form.
2112
         * @throws IllegalArgumentException if the long value does not fit into an integer
2113
         */
2114
        public static Integer convertToInteger(Long longValue) {
2115
                if (longValue < Integer.MIN_VALUE || longValue > Integer.MAX_VALUE) {
1✔
2116
                        throw new IllegalArgumentException(longValue + " cannot be cast to Integer without changing its value.");
×
2117
                }
2118
                return longValue.intValue();
1✔
2119
        }
2120
        
2121
        /**
2122
         * Checks if the passed in date's day of the year is the one that comes immediately before that
2123
         * of the current date
2124
         * 
2125
         * @param date the date to check
2126
         * @since 1.9
2127
         * @return true if the date comes immediately before the current date otherwise false
2128
         */
2129
        public static boolean isYesterday(Date date) {
2130
                if (date == null) {
×
2131
                        return false;
×
2132
                }
2133
                
2134
                Calendar c1 = Calendar.getInstance();
×
2135
                c1.add(Calendar.DAY_OF_YEAR, -1); // yesterday
×
2136
                
2137
                Calendar c2 = Calendar.getInstance();
×
2138
                c2.setTime(date);
×
2139
                
2140
                return (c1.get(Calendar.ERA) == c2.get(Calendar.ERA) && c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR)
×
2141
                        && c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR));
×
2142
        }
2143
        
2144
        /**
2145
         * Get declared field names of a class
2146
         * 
2147
         * @param clazz
2148
         * @return
2149
         */
2150
        public static Set<String> getDeclaredFields(Class<?> clazz) {
2151
                return Arrays.stream(clazz.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet());
1✔
2152
        }
2153

2154
        /**
2155
         * This method checks if a given value is a valid numeric value for the person/patient in subject 
2156
         * given the concept. It checks if a given value is within the valid reference range.
2157
         *
2158
         * @param value The value to check
2159
         * @param obs The observation to be verified
2160
         * @return Error message containing expected range if there was a range mismatch, else returns empty string.
2161
         * 
2162
         * @since 2.7.0
2163
         */
2164
        public static String isValidNumericValue(Float value, Obs obs) {
2165
                ConceptReferenceRange conceptReferenceRange = Context.getConceptService().getConceptReferenceRange(obs.getPerson(), obs.getConcept());
1✔
2166
                if (conceptReferenceRange == null) {
1✔
2167
                        return "";
1✔
2168
                }
2169

2170
                if ((conceptReferenceRange.getHiAbsolute() != null && conceptReferenceRange.getHiAbsolute() < value) ||
1✔
2171
                        (conceptReferenceRange.getLowAbsolute() != null && conceptReferenceRange.getLowAbsolute() > value)) {
1✔
2172
                        return String.format("Expected value between %s and %s", conceptReferenceRange.getLowAbsolute(), conceptReferenceRange.getHiAbsolute());
1✔
2173
                } else {
2174
                        return "";
×
2175
                }
2176
        }
2177
        
2178
}
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