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

openmrs / openmrs-core / 23193642646

17 Mar 2026 12:13PM UTC coverage: 63.1% (-0.3%) from 63.429%
23193642646

push

github

rkorytkowski
Fixing: Fix an issue with the ModuleResourceServlet

0 of 2 new or added lines in 1 file covered. (0.0%)

925 existing lines in 17 files now uncovered.

23137 of 36667 relevant lines covered (63.1%)

0.63 hits per line

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

34.57
/web/src/main/java/org/openmrs/module/web/WebModuleUtil.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.module.web;
11

12
import javax.servlet.Filter;
13
import javax.servlet.ServletConfig;
14
import javax.servlet.ServletContext;
15
import javax.servlet.ServletException;
16
import javax.servlet.ServletRequest;
17
import javax.servlet.http.HttpServlet;
18
import javax.servlet.http.HttpServletRequest;
19
import javax.xml.parsers.DocumentBuilder;
20
import javax.xml.parsers.DocumentBuilderFactory;
21
import javax.xml.parsers.ParserConfigurationException;
22
import javax.xml.transform.Transformer;
23
import javax.xml.transform.TransformerException;
24
import javax.xml.transform.TransformerFactory;
25
import javax.xml.transform.dom.DOMSource;
26
import javax.xml.transform.stream.StreamResult;
27
import java.io.File;
28
import java.io.FileInputStream;
29
import java.io.FileNotFoundException;
30
import java.io.FileOutputStream;
31
import java.io.IOException;
32
import java.io.InputStream;
33
import java.io.OutputStream;
34
import java.io.StringReader;
35
import java.util.ArrayDeque;
36
import java.util.ArrayList;
37
import java.util.Collection;
38
import java.util.Deque;
39
import java.util.Enumeration;
40
import java.util.HashMap;
41
import java.util.Iterator;
42
import java.util.LinkedHashMap;
43
import java.util.List;
44
import java.util.Map;
45
import java.util.Properties;
46
import java.util.concurrent.locks.Lock;
47
import java.util.concurrent.locks.ReentrantLock;
48
import java.util.jar.JarEntry;
49
import java.util.jar.JarFile;
50
import java.util.regex.Pattern;
51

52
import org.openmrs.api.APIException;
53
import org.openmrs.api.context.Context;
54
import org.openmrs.module.Module;
55
import org.openmrs.module.ModuleException;
56
import org.openmrs.module.ModuleFactory;
57
import org.openmrs.module.ModuleUtil;
58
import org.openmrs.module.web.filter.ModuleFilterConfig;
59
import org.openmrs.module.web.filter.ModuleFilterDefinition;
60
import org.openmrs.module.web.filter.ModuleFilterMapping;
61
import org.openmrs.scheduler.SchedulerException;
62
import org.openmrs.scheduler.SchedulerService;
63
import org.openmrs.scheduler.TaskDefinition;
64
import org.openmrs.util.OpenmrsUtil;
65
import org.openmrs.util.PrivilegeConstants;
66
import org.openmrs.web.DispatcherServlet;
67
import org.openmrs.web.StaticDispatcherServlet;
68
import org.slf4j.Logger;
69
import org.slf4j.LoggerFactory;
70
import org.springframework.web.context.support.WebApplicationContextUtils;
71
import org.springframework.web.context.support.XmlWebApplicationContext;
72
import org.w3c.dom.Document;
73
import org.w3c.dom.Element;
74
import org.w3c.dom.NamedNodeMap;
75
import org.w3c.dom.Node;
76
import org.w3c.dom.NodeList;
77
import org.xml.sax.InputSource;
78

79
public class WebModuleUtil {
80

81
        private WebModuleUtil() {
82
        }
83
        
84
        private static final Logger log = LoggerFactory.getLogger(WebModuleUtil.class);
1✔
85
        
86
        private static final Lock SERVLET_LOCK = new ReentrantLock();
1✔
87
        
88
        private static final Lock FILTERS_LOCK = new ReentrantLock();
1✔
89
        
90
        // caches all modules' mapped servlets
91
        private static final Map<String, HttpServlet> MODULE_SERVLETS = new HashMap<>();
1✔
92
        
93
        // caches all modules filters and filter-mappings
94
        private static final Map<Module, Collection<Filter>> MODULE_FILTERS = new HashMap<>();
1✔
95
        
96
        private static final Map<String, Filter> MODULE_FILTERS_BY_NAME = new HashMap<>();
1✔
97
        
98
        private static final Deque<ModuleFilterMapping> MODULE_FILTER_MAPPINGS = new ArrayDeque<>();
1✔
99
        
100
        private static DispatcherServlet dispatcherServlet = null;
1✔
101
        
102
        private static StaticDispatcherServlet staticDispatcherServlet = null;
1✔
103
        
104
        /**
105
         * Performs the webapp specific startup needs for modules Normal startup is done in
106
         * {@link ModuleFactory#startModule(Module)} If delayContextRefresh is true, the spring context
107
         * is not rerun. This will save a lot of time, but it also means that the calling method is
108
         * responsible for restarting the context if necessary (the calling method will also have to
109
         * call {@link #loadServlets(Module, ServletContext)} and
110
         * {@link #loadFilters(Module, ServletContext)}).<br>
111
         * <br>
112
         * If delayContextRefresh is true and this module should have caused a context refresh, a true
113
         * value is returned. Otherwise, false is returned
114
         *
115
         * @param mod Module to start
116
         * @param servletContext the current ServletContext
117
         * @param delayContextRefresh true/false whether or not to do the context refresh
118
         * @return boolean whether or not the spring context need to be refreshed
119
         */
120
        public static boolean startModule(Module mod, ServletContext servletContext, boolean delayContextRefresh) {
121
                
122
                log.debug("Trying to start module {}", mod);
1✔
123
                
124
                // only try and start this module if the api started it without a
125
                // problem.
126
                if (ModuleFactory.isModuleStarted(mod) && !mod.hasStartupError()) {
1✔
127
                        
128
                        String realPath = getRealPath(servletContext);
1✔
129
                        
130
                        if (realPath == null) {
1✔
131
                                realPath = System.getProperty("user.dir");
1✔
132
                        }
133
                        
134
                        File webInf = new File(realPath + "/WEB-INF".replace("/", File.separator));
1✔
135
                        if (!webInf.exists()) {
1✔
136
                                webInf.mkdir();
1✔
137
                        }
138
                        
139
                        // flag to tell whether we added any xml/dwr/etc changes that necessitate a refresh
140
                        // of the web application context
141
                        boolean moduleNeedsContextRefresh = false;
1✔
142
                        
143
                        // copy the html files into the webapp (from /web/module/ in the module)
144
                        // also looks for a spring context file. If found, schedules spring to be restarted
145
                        JarFile jarFile = null;
1✔
146
                        OutputStream outStream = null;
1✔
147
                        InputStream inStream = null;
1✔
148
                        try {
149
                                File modFile = mod.getFile();
1✔
UNCOV
150
                                jarFile = new JarFile(modFile);
×
UNCOV
151
                                Enumeration<JarEntry> entries = jarFile.entries();
×
152
                                
153
                                while (entries.hasMoreElements()) {
×
154
                                        JarEntry entry = entries.nextElement();
×
UNCOV
155
                                        String name = entry.getName();
×
156
                                        log.debug("Entry name: {}", name);
×
157
                                        if (name.startsWith("web/module/")) {
×
158
                                                // trim out the starting path of "web/module/"
159
                                                String filepath = name.substring(11);
×
160
                                                
UNCOV
161
                                                StringBuilder absPath = new StringBuilder(realPath + "/WEB-INF");
×
162
                                                
163
                                                // If this is within the tag file directory, copy it into /WEB-INF/tags/module/moduleId/...
164
                                                if (filepath.startsWith("tags/")) {
×
UNCOV
165
                                                        filepath = filepath.substring(5);
×
166
                                                        absPath.append("/tags/module/");
×
167
                                                }
168
                                                // Otherwise, copy it into /WEB-INF/view/module/moduleId/...
169
                                                else {
UNCOV
170
                                                        absPath.append("/view/module/");
×
171
                                                }
172
                                                
173
                                                // if a module id has a . in it, we should treat that as a /, i.e. files in the module
174
                                                // ui.springmvc should go in folder names like .../ui/springmvc/...
UNCOV
175
                                                absPath.append(mod.getModuleIdAsPath()).append("/").append(filepath);
×
UNCOV
176
                                                log.debug("Moving file from: {} to {}", name, absPath);
×
177
                                                
178
                                                // get the output file
UNCOV
179
                                                File outFile = new File(absPath.toString().replace("/", File.separator));
×
UNCOV
180
                                                if (!outFile.toPath().normalize().startsWith(webInf.toPath().normalize())) {
×
UNCOV
181
                                                        throw new UnsupportedOperationException("Attempted to write file '" + name
×
182
                                                                + "' rejected as it attempts to write outside the WEB-INF directory. This may be the result of a zip-slip style attack.");
183
                                                }
UNCOV
184
                                                if (entry.isDirectory()) {
×
UNCOV
185
                                                        if (!outFile.exists()) {
×
186
                                                                outFile.mkdirs();
×
187
                                                        }
188
                                                } else {
189
                                                        // make the parent directories in case it doesn't exist
UNCOV
190
                                                        File parentDir = outFile.getParentFile();
×
UNCOV
191
                                                        if (!parentDir.exists()) {
×
UNCOV
192
                                                                parentDir.mkdirs();
×
193
                                                        }
194
                                                        
195
                                                        // copy the contents over to the webapp for non directories
UNCOV
196
                                                        outStream = new FileOutputStream(outFile, false);
×
UNCOV
197
                                                        inStream = jarFile.getInputStream(entry);
×
UNCOV
198
                                                        OpenmrsUtil.copyFile(inStream, outStream);
×
199
                                                }
200
                                        } else if ("moduleApplicationContext.xml".equals(name) || "webModuleApplicationContext.xml".equals(name)) {
×
201
                                                moduleNeedsContextRefresh = true;
×
UNCOV
202
                                        } else if (name.equals(mod.getModuleId() + "Context.xml")) {
×
203
                                                String msg = "DEPRECATED: '" + name
×
204
                                                        + "' should be named 'moduleApplicationContext.xml' now. Please update/upgrade. ";
205
                                                throw new ModuleException(msg, mod.getModuleId());
×
206
                                        }
UNCOV
207
                                }
×
208
                        }
209
                        catch (IOException io) {
1✔
210
                                log.warn("Unable to copy files from module " + mod.getModuleId() + " to the web layer", io);
1✔
211
                        }
212
                        finally {
213
                                if (jarFile != null) {
1✔
214
                                        try {
UNCOV
215
                                                jarFile.close();
×
216
                                        }
UNCOV
217
                                        catch (IOException io) {
×
218
                                                log.warn("Couldn't close jar file: " + jarFile.getName(), io);
×
UNCOV
219
                                        }
×
220
                                }
221
                                if (inStream != null) {
1✔
222
                                        try {
UNCOV
223
                                                inStream.close();
×
224
                                        }
UNCOV
225
                                        catch (IOException io) {
×
226
                                                log.warn("Couldn't close InputStream: " + io);
×
UNCOV
227
                                        }
×
228
                                }
229
                                if (outStream != null) {
1✔
230
                                        try {
UNCOV
231
                                                outStream.close();
×
232
                                        }
UNCOV
233
                                        catch (IOException io) {
×
234
                                                log.warn("Couldn't close OutputStream: " + io);
×
UNCOV
235
                                        }
×
236
                                }
237
                        }
238
                        
239
                        // find and add the dwr code to the dwr-modules.xml file (if defined)
240
                        InputStream inputStream = null;
1✔
241
                        try {
242
                                Document config = mod.getConfig();
1✔
243
                                Element root = config.getDocumentElement();
1✔
244
                                if (root.getElementsByTagName("dwr").getLength() > 0) {
1✔
245
                                        
246
                                        // get the dwr-module.xml file that we're appending our code to
247
                                        File f = new File(realPath + "/WEB-INF/dwr-modules.xml".replace("/", File.separator));
1✔
248
                                        
249
                                        // testing if file exists
250
                                        if (!f.exists()) {
1✔
251
                                                // if it does not -> needs to be created
252
                                                createDwrModulesXml(realPath);
1✔
253
                                        }
254
                                        
255
                                        inputStream = new FileInputStream(f);
1✔
256
                                        Document dwrmodulexml = getDWRModuleXML(inputStream, realPath);
1✔
257
                                        Element outputRoot = dwrmodulexml.getDocumentElement();
1✔
258
                                        
259
                                        // loop over all of the children of the "dwr" tag
260
                                        Node node = root.getElementsByTagName("dwr").item(0);
1✔
261
                                        Node current = node.getFirstChild();
1✔
262
                                        
263
                                        while (current != null) {
1✔
264
                                                if ("allow".equals(current.getNodeName()) || "signatures".equals(current.getNodeName())
1✔
265
                                                        || "init".equals(current.getNodeName())) {
1✔
266
                                                        ((Element) current).setAttribute("moduleId", mod.getModuleId());
1✔
267
                                                        outputRoot.appendChild(dwrmodulexml.importNode(current, true));
1✔
268
                                                }
269
                                                
270
                                                current = current.getNextSibling();
1✔
271
                                        }
272
                                        
273
                                        moduleNeedsContextRefresh = true;
1✔
274
                                        
275
                                        // save the dwr-modules.xml file.
276
                                        OpenmrsUtil.saveDocument(dwrmodulexml, f);
1✔
277
                                }
278
                        }
UNCOV
279
                        catch (FileNotFoundException e) {
×
UNCOV
280
                                throw new ModuleException(realPath + "/WEB-INF/dwr-modules.xml file doesn't exist.", e);
×
281
                        }
282
                        finally {
283
                                if (inputStream != null) {
1✔
284
                                        try {
285
                                                inputStream.close();
1✔
286
                                        }
UNCOV
287
                                        catch (IOException io) {
×
UNCOV
288
                                                log.error("Error while closing input stream", io);
×
289
                                        }
1✔
290
                                }
291
                        }
292
                        
293
                        // mark to delete the entire module web directory on exit
294
                        // this will usually only be used when an improper shutdown has occurred.
295
                        String folderPath = realPath + "/WEB-INF/view/module/" + mod.getModuleIdAsPath();
1✔
296
                        File outFile = new File(folderPath.replace("/", File.separator));
1✔
297
                        outFile.deleteOnExit();
1✔
298
                        
299
                        // additional checks on module needing a context refresh
300
                        if (!moduleNeedsContextRefresh && mod.getAdvicePoints() != null && !mod.getAdvicePoints().isEmpty()) {
1✔
301
                                
302
                                // AOP advice points are only loaded during the context refresh now.
303
                                // if the context hasn't been marked to be refreshed yet, mark it
304
                                // now if this module defines some advice
UNCOV
305
                                moduleNeedsContextRefresh = true;
×
306
                                
307
                        }
308
                        
309
                        // refresh the spring web context to get the just-created xml
310
                        // files into it (if we copied an xml file)
311
                        if (moduleNeedsContextRefresh && !delayContextRefresh) {
1✔
UNCOV
312
                                log.debug("Refreshing context for module {}", mod.getModuleId());
×
313
                                
314
                                try {
315
                                        refreshWAC(servletContext, false, mod);
×
UNCOV
316
                                        log.debug("Done refreshing context for module {}", mod.getModuleId());
×
317
                                }
318
                                catch (Exception e) {
×
319
                                        String msg = "Unable to refresh the WebApplicationContext";
×
UNCOV
320
                                        mod.setStartupErrorMessage(msg, e);
×
321
                                        
322
                                        if (log.isWarnEnabled()) {
×
323
                                                log.warn(msg + " for module: " + mod.getModuleId(), e);
×
324
                                        }
325
                                        
326
                                        try {
UNCOV
327
                                                stopModule(mod, servletContext, true);
×
UNCOV
328
                                                ModuleFactory.stopModule(mod, true, true); //remove jar from classloader play
×
329
                                        }
330
                                        catch (Exception e2) {
×
331
                                                // exception expected with most modules here
UNCOV
332
                                                if (log.isWarnEnabled()) {
×
333
                                                        log.warn("Error while stopping a module that had an error on refreshWAC", e2);
×
334
                                                }
335
                                        }
×
336
                                        
337
                                        // try starting the application context again
338
                                        log.debug("Refreshing context for module {} (re-trying)", mod.getModuleId());
×
UNCOV
339
                                        refreshWAC(servletContext, false, mod);
×
UNCOV
340
                                        log.debug("Done refreshing context for module {} (re-trying)", mod.getModuleId());
×
341
                                        
342
                                        notifySuperUsersAboutModuleFailure(mod);
×
343
                                }
×
344
                                
345
                        }
346
                        
347
                        if (!delayContextRefresh && ModuleFactory.isModuleStarted(mod)) {
1✔
348
                                // only loading the servlets/filters if spring is refreshed because one
349
                                // might depend on files being available in spring
350
                                // if the caller wanted to delay the refresh then they are responsible for
351
                                // calling these two methods on the module
352
                                
353
                                // find and cache the module's servlets
354
                                //(only if the module started successfully previously)
UNCOV
355
                                log.debug("Loading servlets and filters for module {}", mod);
×
UNCOV
356
                                servletContext.setAttribute(OpenmrsJspServlet.OPENMRS_TLD_SCAN_NEEDED, true);
×
UNCOV
357
                                loadServlets(mod, servletContext);
×
358
                                loadFilters(mod, servletContext);
×
359
                        }
360
                        
361
                        // return true if the module needs a context refresh and we didn't do it here
362
                        return (moduleNeedsContextRefresh && delayContextRefresh);
1✔
363
                        
364
                }
365
                
366
                // we aren't processing this module, so a context refresh is not necessary
UNCOV
367
                return false;
×
368
        }
369
        
370
        /** Stops all tasks started by given module
371
         * @param mod
372
         */
373
        private static void stopTasks(Module mod) {
374
                SchedulerService schedulerService;
375
                try {
UNCOV
376
                        schedulerService = Context.getSchedulerService();
×
UNCOV
377
                } catch (NullPointerException | APIException e) {
×
378
                        // if we got here, the scheduler has already been shut down, so there's no work to do
379
                        return;
×
380
                }
×
381
                
382
                String modulePackageName = mod.getPackageName();
×
383
                for (TaskDefinition task : schedulerService.getRegisteredTasks()) {
×
384
                        
385
                        String taskClass = task.getTaskClass();
×
386
                        if (isModulePackageNameInTaskClass(modulePackageName, taskClass)) {
×
387
                                try {
388
                                        schedulerService.shutdownTask(task);
×
389
                                }
UNCOV
390
                                catch (SchedulerException e) {
×
391
                                        log.error("Couldn't stop task:" + task + " for module: " + mod);
×
UNCOV
392
                                }
×
393
                        }
394
                }
×
395
        }
×
396
        
397
        /**
398
         * Checks if module package name is in task class name
399
         * @param modulePackageName the package name of module
400
         * @param taskClass the class of given task
401
         * @return true if task and module are in the same package
402
         * <strong>Should</strong> return false for different package names
403
         * <strong>Should</strong> return false if module has longer package name
404
         * <strong>Should</strong> properly match subpackages
405
         * <strong>Should</strong> return false for empty package names
406
         */
407
        public static boolean isModulePackageNameInTaskClass(String modulePackageName, String taskClass) {
408
                return modulePackageName.length() <= taskClass.length()
1✔
409
                        && taskClass.matches(Pattern.quote(modulePackageName) + "(\\..*)+");
1✔
410
        }
411
        
412
        /**
413
         * Send an Alert to all super users that the given module did not start successfully.
414
         *
415
         * @param mod The Module that failed
416
         */
417
        private static void notifySuperUsersAboutModuleFailure(Module mod) {
418
                try {
419
                        // Add the privileges necessary for notifySuperUsers
UNCOV
420
                        Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
UNCOV
421
                        Context.addProxyPrivilege(PrivilegeConstants.GET_USERS);
×
422
                        
423
                        // Send an alert to all administrators
424
                        Context.getAlertService().notifySuperUsers("Module.startupError.notification.message", null, mod.getName());
×
425
                }
426
                finally {
427
                        // Remove added privileges
UNCOV
428
                        Context.removeProxyPrivilege(PrivilegeConstants.GET_USERS);
×
UNCOV
429
                        Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
×
430
                }
431
        }
×
432
        
433
        /**
434
         * This method will find and cache this module's servlets (so that it doesn't have to look them
435
         * up every time)
436
         *
437
         * @param mod
438
         * @param servletContext the servlet context
439
         */
440
        public static void loadServlets(Module mod, ServletContext servletContext) {
441
                Element rootNode = mod.getConfig().getDocumentElement();
1✔
442
                NodeList servletTags = rootNode.getElementsByTagName("servlet");
1✔
443
                
444
                for (int i = 0; i < servletTags.getLength(); i++) {
1✔
445
                        Node node = servletTags.item(i);
1✔
446
                        NodeList childNodes = node.getChildNodes();
1✔
447
                        String name = "", className = "";
1✔
448

449
                        Map<String, String> initParams = new HashMap<>();
1✔
450
                        for (int j = 0; j < childNodes.getLength(); j++) {
1✔
451
                                Node childNode = childNodes.item(j);
1✔
452
                                if ("servlet-name".equals(childNode.getNodeName())) {
1✔
453
                                        if (childNode.getTextContent() != null) {
1✔
454
                                                name = childNode.getTextContent().trim();
1✔
455
                                        }
456
                                } else if ("servlet-class".equals(childNode.getNodeName()) && childNode.getTextContent() != null) {
1✔
457
                                        className = childNode.getTextContent().trim();
1✔
458
                                } else if ("init-param".equals(childNode.getNodeName())) {
1✔
459
                                        NodeList initParamChildren = childNode.getChildNodes();
1✔
460
                                        String paramName = null, paramValue = null;
1✔
461
                                        for (int k = 0; k < initParamChildren.getLength(); k++) {
1✔
462
                                                Node initParamChild = initParamChildren.item(k);
1✔
463
                                                if ("param-name".equals(initParamChild.getNodeName()) && initParamChild.getTextContent() != null) {
1✔
464
                                                        paramName = initParamChild.getTextContent().trim();
1✔
465
                                                } else if ("param-value".equals(initParamChild.getNodeName()) && initParamChild.getTextContent() != null) {
1✔
466
                                                        paramValue = initParamChild.getTextContent().trim();
1✔
467
                                                }
468
                                        }
469

470
                                        if (paramName != null && paramValue != null) {
1✔
471
                                                initParams.put(paramName, paramValue);
1✔
472
                                        }
473
                                }
474
                        }
475
                        if (name.length() == 0 || className.length() == 0) {
1✔
UNCOV
476
                                log.warn("both 'servlet-name' and 'servlet-class' are required for the 'servlet' tag. Given '" + name
×
UNCOV
477
                                        + "' and '" + className + "' for module " + mod.getName());
×
UNCOV
478
                                continue;
×
479
                        }
480
                        
481
                        HttpServlet httpServlet;
482
                        try {
483
                                httpServlet = (HttpServlet) ModuleFactory.getModuleClassLoader(mod).loadClass(className).newInstance();
1✔
484
                        }
UNCOV
485
                        catch (ClassCastException e) {
×
UNCOV
486
                                log.warn("Class {} from module {} is not a valid HttpServlet", className, mod, e);
×
UNCOV
487
                                continue;
×
488
                        }
489
                        catch (ClassNotFoundException e) {
×
490
                                log.warn("Class {} not found for servlet {} from module {}", className, name, mod, e);
×
UNCOV
491
                                continue;
×
492
                        }
493
                        catch (IllegalAccessException e) {
×
494
                                log.warn("Class {} cannot be accessed for servlet {} from module {}", className, name, mod, e);
×
UNCOV
495
                                continue;
×
496
                        }
497
                        catch (InstantiationException e) {
×
498
                                log.warn("Class {} cannot be instantiated for servlet {} from module {}", className, name, mod, e);
×
UNCOV
499
                                continue;
×
500
                        }
1✔
501
                        
502
                        try {
503
                                log.debug("Initializing {} servlet. - {}.", name, httpServlet);
1✔
504
                                ServletConfig servletConfig = new ModuleServlet.SimpleServletConfig(name, servletContext, initParams);
1✔
505
                                httpServlet.init(servletConfig);
1✔
506
                        }
UNCOV
507
                        catch (Exception e) {
×
UNCOV
508
                                log.warn("Unable to initialize servlet {}", name, e);
×
UNCOV
509
                                throw new ModuleException("Unable to initialize servlet " + name, mod.getModuleId(), e);
×
510
                        }
1✔
511
                        
512
                        // don't allow modules to overwrite servlets of other modules.
513
                        HttpServlet otherServletUsingSameName = MODULE_SERVLETS.get(name);
1✔
514
                        if (otherServletUsingSameName != null) {
1✔
UNCOV
515
                                String otherServletName = otherServletUsingSameName.getClass().getName();
×
UNCOV
516
                                throw new ModuleException("A servlet mapping with name " + name + " is already in use and pointing at: "
×
517
                                        + otherServletName + " from another installed module and this module is trying"
518
                                        + " to use that same name.  Either the module attempting to be installed (" + mod
519
                                        + ") will not work or the other one will not.  Please consult the developers of these two"
520
                                        + " modules to sort this out.");
521
                        }
522
                        
523
                        log.debug("Caching the {} servlet.", name);
1✔
524
                        
525
                        SERVLET_LOCK.lock();
1✔
526
                        try {
527
                                MODULE_SERVLETS.put(name, httpServlet);
1✔
528
                        } finally {
529
                                SERVLET_LOCK.unlock();
1✔
530
                        }
531
                }
532
        }
1✔
533
        
534
        /**
535
         * Remove the servlets defined for this module
536
         *
537
         * @param mod the module that is being stopped that needs its servlets removed
538
         */
539
        public static void unloadServlets(Module mod) {
540
                Element rootNode = mod.getConfig().getDocumentElement();
1✔
541
                NodeList servletTags = rootNode.getElementsByTagName("servlet");
1✔
542
                
543
                for (int i = 0; i < servletTags.getLength(); i++) {
1✔
544
                        Node node = servletTags.item(i);
1✔
545
                        NodeList childNodes = node.getChildNodes();
1✔
546
                        String name;
547
                        for (int j = 0; j < childNodes.getLength(); j++) {
1✔
548
                                Node childNode = childNodes.item(j);
1✔
549
                                if ("servlet-name".equals(childNode.getNodeName()) && childNode.getTextContent() != null) {
1✔
550
                                        name = childNode.getTextContent().trim();
1✔
551
                                        
552
                                        HttpServlet servlet;
553
                                        SERVLET_LOCK.lock();
1✔
554
                                        try {
555
                                                servlet = MODULE_SERVLETS.get(name);
1✔
556
                                        } finally {
557
                                                SERVLET_LOCK.unlock();
1✔
558
                                        }
559
                                        
560
                                        if (servlet != null) {
1✔
561
                                                // shut down the servlet
562
                                                servlet.destroy();
1✔
563
                                        }
564
                                        
565
                                        SERVLET_LOCK.lock();
1✔
566
                                        try {
567
                                                MODULE_SERVLETS.remove(name);
1✔
568
                                        } finally {
569
                                                SERVLET_LOCK.unlock();
1✔
570
                                        }
571
                                }
572
                        }
573
                }
574
        }
1✔
575
        
576
        /**
577
         * This method will initialize and store this module's filters
578
         *
579
         * @param module - The Module to load and register Filters
580
         * @param servletContext - The servletContext within which this method is called
581
         */
582
        public static void loadFilters(Module module, ServletContext servletContext) {
583
                
584
                // Load Filters
UNCOV
585
                Map<String, Filter> filters = new LinkedHashMap<>();
×
586
                
587
                Map<String, Filter> existingFilters;
588
                FILTERS_LOCK.lock();
×
589
                try {
UNCOV
590
                        existingFilters = new HashMap<>(MODULE_FILTERS_BY_NAME);
×
591
                } finally {
UNCOV
592
                        FILTERS_LOCK.unlock();
×
593
                }
594
                
595
                for (ModuleFilterDefinition def : ModuleFilterDefinition.retrieveFilterDefinitions(module)) {
×
UNCOV
596
                        String name = def.getFilterName();
×
UNCOV
597
                        String className = def.getFilterClass();
×
598
                        
599
                        if (existingFilters.containsKey(name)) {
×
600
                                throw new ModuleException("A filter with the name " + name + " is already in use and pointing at: "
×
UNCOV
601
                                        + existingFilters.get(name).getClass().getName()
×
602
                                        + " from another installed module and this module is trying"
603
                                        + " to use that same name.  Either the module attempting to be installed (" + module
604
                                        + ") will not work or the other one will not.  Please consult the developers of these two"
605
                                        + " modules to sort this out.");
606
                        }
607
                        
UNCOV
608
                        ModuleFilterConfig config = ModuleFilterConfig.getInstance(def, servletContext);
×
609
                        
610
                        Filter filter;
611
                        try {
UNCOV
612
                                filter = (Filter) ModuleFactory.getModuleClassLoader(module).loadClass(className).newInstance();
×
613
                        }
UNCOV
614
                        catch (ClassCastException e) {
×
615
                                log.warn("Class {} from module {} is not a valid Filter", className, module, e);
×
UNCOV
616
                                continue;
×
617
                        }
618
                        catch (ClassNotFoundException e) {
×
619
                                log.warn("Class {} not found for servlet {} from module {}", className, name, module, e);
×
UNCOV
620
                                continue;
×
621
                        }
622
                        catch (IllegalAccessException e) {
×
623
                                log.warn("Class {} cannot be accessed for servlet {} from module {}", className, name, module, e);
×
UNCOV
624
                                continue;
×
625
                        }
626
                        catch (InstantiationException e) {
×
627
                                log.warn("Class {} cannot be instantiated for servlet {} from module {}", className, name, module, e);
×
UNCOV
628
                                continue;
×
629
                        }
×
630
                        
631
                        try {
632
                                log.debug("Initializing {} filter. - {}.", name, filter);
×
UNCOV
633
                                filter.init(config);
×
634
                        }
635
                        catch (Exception e) {
×
636
                                log.warn("Unable to initialize servlet {}", name, e);
×
UNCOV
637
                                throw new ModuleException("Unable to initialize servlet " + name, module.getModuleId(), e);
×
638
                        }
×
639
                        
640
                        filters.put(name, filter);
×
641
                }
×
642

643
                FILTERS_LOCK.lock();
×
644
                try {
UNCOV
645
                        MODULE_FILTERS.put(module, filters.values());
×
646
                        MODULE_FILTERS_BY_NAME.putAll(filters);
×
UNCOV
647
                        log.debug("Module {} successfully loaded {} filters.", module, filters.size());
×
648
                        
649
                        // Load Filter Mappings
650
                        Deque<ModuleFilterMapping> modMappings = ModuleFilterMapping.retrieveFilterMappings(module);
×
651
                        
652
                        // IMPORTANT: Filter load order
653
                        // retrieveFilterMappings will return the list of filters in the order they occur in the config.xml file
654
                        // here we add them to the *front* of the filter mappings
UNCOV
655
                        modMappings.descendingIterator().forEachRemaining(MODULE_FILTER_MAPPINGS::addFirst);
×
656
                        
UNCOV
657
                        log.debug("Module {} successfully loaded {} filter mappings.", module, modMappings.size());
×
658
                } finally {
UNCOV
659
                        FILTERS_LOCK.unlock();
×
660
                }
UNCOV
661
        }
×
662
        
663
        /**
664
         * This method will destroy and remove all filters that were registered by the passed
665
         * {@link Module}
666
         *
667
         * @param module - The Module for which you want to remove and destroy filters.
668
         */
669
        public static void unloadFilters(Module module) {
670
                
671
                // Unload Filter Mappings
UNCOV
672
                for (Iterator<ModuleFilterMapping> mapIter = MODULE_FILTER_MAPPINGS.iterator(); mapIter.hasNext();) {
×
UNCOV
673
                        ModuleFilterMapping mapping = mapIter.next();
×
UNCOV
674
                        if (module.equals(mapping.getModule())) {
×
675
                                mapIter.remove();
×
676
                                log.debug("Removed ModuleFilterMapping: " + mapping);
×
677
                        }
678
                }
×
679
                
680
                // unload Filters
681
                Collection<Filter> filters = MODULE_FILTERS.get(module);
×
UNCOV
682
                if (filters != null) {
×
683
                        try {
684
                                for (Filter f : filters) {
×
685
                                        f.destroy();
×
UNCOV
686
                                }
×
687
                        }
688
                        catch (Exception e) {
×
689
                                log.warn("An error occurred while trying to destroy and remove module Filter.", e);
×
UNCOV
690
                        }
×
691
                        
692
                        log.debug("Module: " + module.getModuleId() + " successfully unloaded " + filters.size() + " filters.");
×
693
                        MODULE_FILTERS.remove(module);
×
694

695
                        MODULE_FILTERS_BY_NAME.values().removeIf(filters::contains);
×
696
                }
UNCOV
697
        }
×
698
        
699
        /**
700
         * This method will return all Filters that have been registered a module
701
         *
702
         * @return A Collection of {@link Filter}s that have been registered by a module
703
         */
704
        public static Collection<Filter> getFilters() {
UNCOV
705
                return MODULE_FILTERS_BY_NAME.values();
×
706
        }
707
        
708
        /**
709
         * This method will return all Filter Mappings that have been registered by a module
710
         *
711
         * @return A Collection of all {@link ModuleFilterMapping}s that have been registered by a
712
         *         Module
713
         */
714
        public static Collection<ModuleFilterMapping> getFilterMappings() {
UNCOV
715
                return new ArrayList<>(MODULE_FILTER_MAPPINGS);
×
716
        }
717
        
718
        /**
719
         * Return List of Filters that have been loaded through Modules that have mappings that pass for
720
         * the passed request
721
         *
722
         * @param request - The request to check for matching {@link Filter}s
723
         * @return List of all {@link Filter}s that have filter mappings that match the passed request
724
         */
725
        public static List<Filter> getFiltersForRequest(ServletRequest request) {
726
                
UNCOV
727
                List<Filter> filters = new ArrayList<>();
×
UNCOV
728
                if (request != null) {
×
UNCOV
729
                        HttpServletRequest httpRequest = (HttpServletRequest) request;
×
730
                        String requestPath = httpRequest.getRequestURI();
×
731
                        
732
                        if (requestPath != null) {
×
733
                                if (requestPath.startsWith(httpRequest.getContextPath())) {
×
UNCOV
734
                                        requestPath = requestPath.substring(httpRequest.getContextPath().length());
×
735
                                }
736
                                for (ModuleFilterMapping filterMapping : WebModuleUtil.getFilterMappings()) {
×
737
                                        if (ModuleFilterMapping.filterMappingPasses(filterMapping, requestPath)) {
×
UNCOV
738
                                                Filter passedFilter = MODULE_FILTERS_BY_NAME.get(filterMapping.getFilterName());
×
739
                                                if (passedFilter != null) {
×
740
                                                        filters.add(passedFilter);
×
741
                                                } else {
742
                                                        log.warn("Unable to retrieve filter that has a name of " + filterMapping.getFilterName()
×
743
                                                                + " in filter mapping.");
744
                                                }
745
                                        }
UNCOV
746
                                }
×
747
                        }
748
                }
749
                return filters;
×
750
        }
751
        
752
        /**
753
         * @param inputStream
754
         * @param realPath
755
         * @return
756
         */
757
        private static Document getDWRModuleXML(InputStream inputStream, String realPath) {
758
                Document dwrmodulexml;
759
                try {
760
                        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
1✔
761
                        DocumentBuilder db = dbf.newDocumentBuilder();
1✔
762

763
                        // When asked to resolve external entities (such as a DTD) we return an InputSource
764
                        // with no data at the end, causing the parser to ignore the DTD.
765
                        db.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader("")));
1✔
766
                        dwrmodulexml = db.parse(inputStream);
1✔
767
                }
UNCOV
768
                catch (Exception e) {
×
UNCOV
769
                        throw new ModuleException("Error parsing dwr-modules.xml file", e);
×
770
                }
1✔
771
                
772
                return dwrmodulexml;
1✔
773
        }
774
        
775
        /**
776
         * Reverses all activities done by startModule(org.openmrs.module.Module) Normal stop/shutdown
777
         * is done by ModuleFactory
778
         */
779
        public static void shutdownModules(ServletContext servletContext) {
780
                
UNCOV
781
                String realPath = getRealPath(servletContext);
×
782
                
783
                // clear the module messages
784
                String messagesPath = realPath + "/WEB-INF/";
×
UNCOV
785
                File folder = new File(messagesPath.replace("/", File.separator));
×
786
                
787
                File[] files = folder.listFiles();
×
788
                if (folder.exists() && files != null) {
×
UNCOV
789
                        Properties emptyProperties = new Properties();
×
790
                        for (File f : files) {
×
791
                                if (f.getName().startsWith("module_messages")) {
×
792
                                        OpenmrsUtil.storeProperties(emptyProperties, f, "");
×
793
                                }
794
                        }
795
                }
796
                
797
                // call web shutdown for each module
UNCOV
798
                for (Module mod : ModuleFactory.getLoadedModules()) {
×
UNCOV
799
                        stopModule(mod, servletContext, true);
×
UNCOV
800
                }
×
801
                
802
        }
×
803
        
804
        /**
805
         * Reverses all visible activities done by startModule(org.openmrs.module.Module)
806
         *
807
         * @param mod
808
         * @param servletContext
809
         */
810
        public static void stopModule(Module mod, ServletContext servletContext) {
UNCOV
811
                stopModule(mod, servletContext, false);
×
UNCOV
812
        }
×
813
        
814
        /**
815
         * Reverses all visible activities done by startModule(org.openmrs.module.Module)
816
         *
817
         * @param mod
818
         * @param servletContext
819
         * @param skipRefresh
820
         */
821
        public static void stopModule(Module mod, ServletContext servletContext, boolean skipRefresh) {
822
                
UNCOV
823
                String moduleId = mod.getModuleId();
×
UNCOV
824
                String modulePackage = mod.getPackageName();
×
825
                
826
                // stop all dependent modules
827
                for (Module dependentModule : ModuleFactory.getStartedModules()) {
×
UNCOV
828
                        if (!dependentModule.equals(mod) && dependentModule.getRequiredModules().contains(modulePackage)) {
×
UNCOV
829
                                stopModule(dependentModule, servletContext, skipRefresh);
×
830
                        }
831
                }
×
832
                
UNCOV
833
                String realPath = getRealPath(servletContext);
×
834
                
835
                // delete the web files from the webapp
836
                String absPath = realPath + "/WEB-INF/view/module/" + moduleId;
×
UNCOV
837
                File moduleWebFolder = new File(absPath.replace("/", File.separator));
×
UNCOV
838
                if (moduleWebFolder.exists()) {
×
839
                        try {
840
                                OpenmrsUtil.deleteDirectory(moduleWebFolder);
×
841
                        }
UNCOV
842
                        catch (IOException io) {
×
843
                                log.warn("Couldn't delete: " + moduleWebFolder.getAbsolutePath(), io);
×
UNCOV
844
                        }
×
845
                }
846
                
847
                // (not) deleting module message properties
848
                
849
                // remove the module's servlets
UNCOV
850
                unloadServlets(mod);
×
851
                
852
                // remove the module's filters and filter mappings
853
                unloadFilters(mod);
×
854
                
855
                // stop all tasks associated with mod
856
                stopTasks(mod);
×
857
                
858
                // remove this module's entries in the dwr xml file
859
                InputStream inputStream = null;
×
860
                try {
UNCOV
861
                        Document config = mod.getConfig();
×
862
                        Element root = config.getDocumentElement();
×
863
                        // if they defined any xml element
864
                        if (root.getElementsByTagName("dwr").getLength() > 0) {
×
865
                                
866
                                // get the dwr-module.xml file that we're appending our code to
867
                                File f = new File(realPath + "/WEB-INF/dwr-modules.xml".replace("/", File.separator));
×
868
                                
869
                                // testing if file exists
870
                                if (!f.exists()) {
×
871
                                        // if it does not -> needs to be created
UNCOV
872
                                        createDwrModulesXml(realPath);
×
873
                                }
874
                                
875
                                inputStream = new FileInputStream(f);
×
UNCOV
876
                                Document dwrmodulexml = getDWRModuleXML(inputStream, realPath);
×
UNCOV
877
                                Element outputRoot = dwrmodulexml.getDocumentElement();
×
878
                                
879
                                // loop over all of the children of the "dwr" tag
880
                                // and remove all "allow" and "signature" tags that have the
881
                                // same moduleId attr as the module being stopped
UNCOV
882
                                NodeList nodeList = outputRoot.getChildNodes();
×
UNCOV
883
                                int i = 0;
×
UNCOV
884
                                while (i < nodeList.getLength()) {
×
885
                                        Node current = nodeList.item(i);
×
886
                                        if ("allow".equals(current.getNodeName()) || "signatures".equals(current.getNodeName())) {
×
887
                                                NamedNodeMap attrs = current.getAttributes();
×
888
                                                Node attr = attrs.getNamedItem("moduleId");
×
889
                                                if (attr != null && moduleId.equals(attr.getNodeValue())) {
×
890
                                                        outputRoot.removeChild(current);
×
891
                                                } else {
892
                                                        i++;
×
893
                                                }
UNCOV
894
                                        } else {
×
895
                                                i++;
×
896
                                        }
897
                                }
×
898
                                
899
                                // save the dwr-modules.xml file.
900
                                OpenmrsUtil.saveDocument(dwrmodulexml, f);
×
901
                        }
902
                }
903
                catch (FileNotFoundException e) {
×
UNCOV
904
                        throw new ModuleException(realPath + "/WEB-INF/dwr-modules.xml file doesn't exist.", e);
×
905
                }
906
                finally {
907
                        if (inputStream != null) {
×
908
                                try {
UNCOV
909
                                        inputStream.close();
×
910
                                }
UNCOV
911
                                catch (IOException io) {
×
912
                                        log.error("Error while closing input stream", io);
×
UNCOV
913
                                }
×
914
                        }
915
                }
916
                
UNCOV
917
                if (!skipRefresh) {        
×
UNCOV
918
                        refreshWAC(servletContext, false, null);
×
919
                }
920
                
921
        }
×
922
        
923
        /**
924
         * Stops, closes, and refreshes the Spring context for the given <code>servletContext</code>
925
         *
926
         * @param servletContext
927
         * @param isOpenmrsStartup if this refresh is being done at application startup
928
         * @param startedModule the module that was just started and waiting on the context refresh
929
         * @return The newly refreshed webApplicationContext
930
         */
931
        public static XmlWebApplicationContext refreshWAC(ServletContext servletContext, boolean isOpenmrsStartup,
932
                Module startedModule) {
UNCOV
933
                XmlWebApplicationContext wac = (XmlWebApplicationContext) WebApplicationContextUtils
×
UNCOV
934
                        .getWebApplicationContext(servletContext);
×
UNCOV
935
                log.debug("Refreshing Web Application Context of class: {}", wac.getClass().getName());
×
936
                
937
                if (dispatcherServlet != null) {
×
938
                        dispatcherServlet.stopAndCloseApplicationContext();
×
939
                }
940
                
941
                if (staticDispatcherServlet != null) {
×
UNCOV
942
                        staticDispatcherServlet.stopAndCloseApplicationContext();
×
943
                }
944
                
945
                XmlWebApplicationContext newAppContext = (XmlWebApplicationContext) ModuleUtil.refreshApplicationContext(wac,
×
946
                    isOpenmrsStartup, startedModule);
947
                
948
                try {
949
                        // must "refresh" the spring dispatcherservlet as well to add in
950
                        //the new handlerMappings
UNCOV
951
                        if (dispatcherServlet != null) {
×
UNCOV
952
                                dispatcherServlet.reInitFrameworkServlet();
×
953
                        }
954
                        
955
                        if (staticDispatcherServlet != null) {
×
UNCOV
956
                                staticDispatcherServlet.refreshApplicationContext();
×
957
                        }
958
                }
959
                catch (ServletException se) {
×
UNCOV
960
                        log.warn("Caught a servlet exception while refreshing the dispatcher servlet", se);
×
UNCOV
961
                }
×
962
                
963
                return newAppContext;
×
964
        }
965
        
966
        /**
967
         * Save the dispatcher servlet for use later (reinitializing things)
968
         *
969
         * @param ds
970
         */
971
        public static void setDispatcherServlet(DispatcherServlet ds) {
972
                log.debug("Setting dispatcher servlet: " + ds);
1✔
973
                dispatcherServlet = ds;
1✔
974
        }
1✔
975
        
976
        /**
977
         * Save the static content dispatcher servlet for use later when refreshing spring
978
         *
979
         * @param ds
980
         */
981
        public static void setStaticDispatcherServlet(StaticDispatcherServlet ds) {
UNCOV
982
                log.debug("Setting dispatcher servlet for static content: " + ds);
×
UNCOV
983
                staticDispatcherServlet = ds;
×
UNCOV
984
        }
×
985
        
986
        /**
987
         * Finds the servlet defined by the servlet name
988
         *
989
         * @param servletName the name of the servlet out of the path
990
         * @return the current servlet or null if none defined
991
         */
992
        public static HttpServlet getServlet(String servletName) {
993
                return MODULE_SERVLETS.get(servletName);
1✔
994
        }
995
        
996
        /**
997
         * Retrieves a path to a folder that stores web files of a module. <br>
998
         * (path-to-openmrs/WEB-INF/view/module/moduleid)
999
         *
1000
         * @param moduleId module id (e.g., "basicmodule")
1001
         * @return a path to a folder that stores web files or null if not in a web environment
1002
         * <strong>Should</strong> return the correct module folder
1003
         * <strong>Should</strong> return null if the dispatcher servlet is not yet set
1004
         * <strong>Should</strong> return the correct module folder if real path has a trailing slash
1005
         */
1006
        public static String getModuleWebFolder(String moduleId) {
1007
                if (dispatcherServlet == null) {
1✔
1008
                        throw new ModuleException("Dispatcher servlet must be present in the web environment");
1✔
1009
                }
1010
                
1011
                String moduleFolder = "WEB-INF/view/module/";
1✔
1012
                String realPath = dispatcherServlet.getServletContext().getRealPath("");
1✔
1013
                String moduleWebFolder;
1014
                
1015
                //RealPath may contain '/' on Windows when running tests with the mocked servlet context
1016
                if (realPath.endsWith(File.separator) || realPath.endsWith("/")) {
1✔
1017
                        moduleWebFolder = realPath + moduleFolder;
1✔
1018
                } else {
1019
                        moduleWebFolder = realPath + "/" + moduleFolder;
1✔
1020
                }
1021
                
1022
                moduleWebFolder += moduleId;
1✔
1023
                
1024
                return moduleWebFolder.replace("/", File.separator);
1✔
1025
        }
1026
        
1027
        public static void createDwrModulesXml(String realPath) {
1028
                
1029
                try {
1030
                        
1031
                        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
1✔
1032
                        DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
1✔
1033
                        
1034
                        // root elements
1035
                        Document doc = docBuilder.newDocument();
1✔
1036
                        Element rootElement = doc.createElement("dwr");
1✔
1037
                        doc.appendChild(rootElement);
1✔
1038
                        
1039
                        // write the content into xml file
1040
                        TransformerFactory transformerFactory = TransformerFactory.newInstance();
1✔
1041
                        Transformer transformer = transformerFactory.newTransformer();
1✔
1042
                        DOMSource source = new DOMSource(doc);
1✔
1043
                        StreamResult result = new StreamResult(new File(realPath
1✔
1044
                                + "/WEB-INF/dwr-modules.xml".replace("/", File.separator)));
1✔
1045
                        
1046
                        transformer.transform(source, result);
1✔
1047
                        
1048
                }
UNCOV
1049
                catch (ParserConfigurationException pce) {
×
UNCOV
1050
                        log.error("Failed to parse document", pce);
×
1051
                }
1052
                catch (TransformerException tfe) {
×
1053
                        log.error("Failed to transorm xml source", tfe);
×
1054
                }
1✔
1055
        }
1✔
1056

1057
        public static String getRealPath(ServletContext servletContext) {
1058
                return servletContext.getRealPath("");
1✔
1059
        }
1060
        
1061
}
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