• 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

9.68
/web/src/main/java/org/openmrs/web/filter/StartupFilter.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.web.filter;
11

12
import java.io.FileInputStream;
13
import java.io.FileNotFoundException;
14
import java.io.IOException;
15
import java.io.InputStream;
16
import java.io.InputStreamReader;
17
import java.lang.reflect.Field;
18
import java.net.HttpURLConnection;
19
import java.net.URL;
20
import java.nio.charset.StandardCharsets;
21
import java.nio.file.Files;
22
import java.nio.file.Path;
23
import java.nio.file.Paths;
24
import java.util.Collections;
25
import java.util.HashMap;
26
import java.util.List;
27
import java.util.Locale;
28
import java.util.Map;
29
import java.util.Properties;
30

31
import javax.servlet.Filter;
32
import javax.servlet.FilterChain;
33
import javax.servlet.FilterConfig;
34
import javax.servlet.ServletContext;
35
import javax.servlet.ServletException;
36
import javax.servlet.ServletRequest;
37
import javax.servlet.ServletResponse;
38
import javax.servlet.http.HttpServletRequest;
39
import javax.servlet.http.HttpServletResponse;
40

41
import com.fasterxml.jackson.databind.ObjectMapper;
42
import org.apache.commons.lang3.ArrayUtils;
43
import org.apache.velocity.VelocityContext;
44
import org.apache.velocity.app.VelocityEngine;
45
import org.apache.velocity.runtime.RuntimeConstants;
46
import org.apache.velocity.runtime.log.CommonsLogLogChute;
47
import org.apache.velocity.tools.Scope;
48
import org.apache.velocity.tools.ToolContext;
49
import org.apache.velocity.tools.ToolManager;
50
import org.apache.velocity.tools.config.DefaultKey;
51
import org.apache.velocity.tools.config.FactoryConfiguration;
52
import org.apache.velocity.tools.config.ToolConfiguration;
53
import org.apache.velocity.tools.config.ToolboxConfiguration;
54
import org.openmrs.OpenmrsCharacterEscapes;
55
import org.openmrs.api.APIException;
56
import org.openmrs.api.context.Context;
57
import org.openmrs.logging.MemoryAppender;
58
import org.openmrs.logging.OpenmrsLoggingUtil;
59
import org.openmrs.util.LocaleUtility;
60
import org.openmrs.util.OpenmrsUtil;
61
import org.openmrs.web.Listener;
62
import org.openmrs.web.WebConstants;
63
import org.openmrs.web.filter.initialization.InitializationFilter;
64
import org.openmrs.web.filter.update.UpdateFilter;
65
import org.openmrs.web.filter.util.FilterUtil;
66
import org.openmrs.web.filter.util.LocalizationTool;
67
import org.slf4j.Logger;
68
import org.slf4j.LoggerFactory;
69
import org.springframework.http.MediaType;
70

71
/**
72
 * Abstract class used when a small wizard is needed before Spring, jsp, etc has been started up.
73
 *
74
 * @see UpdateFilter
75
 * @see InitializationFilter
76
 */
77
public abstract class StartupFilter implements Filter {
1✔
78
        
79
        private static final Logger log = LoggerFactory.getLogger(StartupFilter.class);
1✔
80
        
81
        protected static VelocityEngine velocityEngine = null;
1✔
82
        
83
        public static final String AUTO_RUN_OPENMRS = "auto_run_openmrs";
84
        
85
        /**
86
         * Set by the {@link #init(FilterConfig)} method so that we have access to the current
87
         * {@link ServletContext}
88
         */
89
        protected FilterConfig filterConfig = null;
1✔
90
        
91
        /**
92
         * Records errors that will be displayed to the user
93
         */
94
        protected Map<String, Object[]> errors = new HashMap<>();
1✔
95
        
96
        /**
97
         * Messages that will be displayed to the user
98
         */
99
        protected Map<String, Object[]> msgs = new HashMap<>();
1✔
100
        
101
        /**
102
         * Used for configuring tools within velocity toolbox
103
         */
104
        private ToolContext toolContext = null;
1✔
105
        
106
        /**
107
         * The web.xml file sets this {@link StartupFilter} to be the first filter for all requests.
108
         *
109
         * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse,
110
         *      javax.servlet.FilterChain)
111
         */
112
        @Override
113
        public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
114
                throws IOException, ServletException {
115
                if (((HttpServletRequest) request).getServletPath().equals("/health/started")) {
1✔
UNCOV
116
                        ((HttpServletResponse) response).setStatus(Listener.isOpenmrsStarted() ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE);
×
117
                } else if (((HttpServletRequest) request).getServletPath().equals("/health/alive")) {
1✔
118
                        if (Listener.isSetupNeeded() && !InitializationFilter.isInstallationStarted()) {
1✔
119
                                triggerSetup((HttpServletRequest) request);
×
120
                        }
121
                        boolean isOpenmrsAlive = Listener.isOpenmrsStarted() || Listener.isSetupNeeded() 
1✔
122
                                || InitializationFilter.isInstallationStarted();
1✔
123
                        ((HttpServletResponse) response).setStatus(isOpenmrsAlive ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE);
1✔
124
                        
125
                }
1✔
126
                else if (skipFilter((HttpServletRequest) request)) {
×
UNCOV
127
                        chain.doFilter(request, response);
×
128
                } else {
129
                        
130
                        HttpServletRequest httpRequest = (HttpServletRequest) request;
×
UNCOV
131
                        HttpServletResponse httpResponse = (HttpServletResponse) response;
×
132
                        
UNCOV
133
                        String servletPath = httpRequest.getServletPath();
×
134
                        // for all /images and /initfilter/scripts files, write the path
135
                        // (the "/initfilter" part is needed so that the openmrs_static_context-servlet.xml file doesn't
136
                        //  get instantiated early, before the locale messages are all set up)
137
                        if (servletPath.startsWith("/images") || servletPath.startsWith("/initfilter/scripts")) {
×
138
                                // strip out the /initfilter part
139
                                servletPath = servletPath.replaceFirst("/initfilter", "/WEB-INF/view");
×
140
                                // writes the actual file path to the response
141
                                Path filePath = Paths.get(filterConfig.getServletContext().getRealPath(servletPath)).normalize();
×
UNCOV
142
                                Path fullFilePath = filePath;
×
143
                                
UNCOV
144
                                if (httpRequest.getPathInfo() != null) {
×
145
                                        fullFilePath = fullFilePath.resolve(httpRequest.getPathInfo());
×
146
                                        if (!(fullFilePath.normalize().startsWith(filePath))) {
×
UNCOV
147
                                                log.warn("Detected attempted directory traversal in request for {}", httpRequest.getPathInfo());
×
148
                                                return;
×
149
                                        }
150
                                }
151
                                
UNCOV
152
                                String contentType = httpRequest.getServletContext().getMimeType(fullFilePath.toString());
×
153
                                if (contentType == null || contentType.isEmpty()) {
×
154
                                        try {
UNCOV
155
                                                contentType = Files.probeContentType(fullFilePath);
×
156
                                        } catch (IOException ignored) {}
×
157
                                }
158

159
                                MediaType mediaType;
UNCOV
160
                                if (contentType != null && !contentType.isEmpty()) {
×
161
                                        mediaType = MediaType.parseMediaType(contentType);
×
162
                                } else {
UNCOV
163
                                        mediaType = MediaType.APPLICATION_OCTET_STREAM;
×
164
                                }
165
                                
UNCOV
166
                                response.setContentType(mediaType.toString());
×
167
                                
168
                                try (InputStream fis = new FileInputStream(fullFilePath.normalize().toFile())) {
×
169
                                        OpenmrsUtil.copyFile(fis, httpResponse.getOutputStream());
×
170
                                }
171
                                catch (FileNotFoundException e) {
×
UNCOV
172
                                        log.error("Unable to find file: {}", filePath, e);
×
173
                                }
UNCOV
174
                                catch (IOException e) {
×
UNCOV
175
                                        log.warn("An error occurred while handling file {}", filePath, e);
×
176
                                }
×
177
                        } else if (servletPath.startsWith("/scripts")) {
×
UNCOV
178
                                log.error(
×
179
                                    "Calling /scripts during the initializationfilter pages will cause the openmrs_static_context-servlet.xml to initialize too early and cause errors after startup.  Use '/initfilter"
180
                                            + servletPath + "' instead.");
181
                        }
182
                        // for anything but /initialsetup
183
                        else if (!httpRequest.getServletPath().equals("/" + WebConstants.SETUP_PAGE_URL)
×
184
                                && !httpRequest.getServletPath().equals("/" + AUTO_RUN_OPENMRS)) {
×
185
                                // send the user to the setup page
186
                                httpResponse.sendRedirect("/" + WebConstants.WEBAPP_NAME + "/" + WebConstants.SETUP_PAGE_URL);
×
187
                        } else {
188
                                
UNCOV
189
                                if ("GET".equals(httpRequest.getMethod())) {
×
UNCOV
190
                                        doGet(httpRequest, httpResponse);
×
UNCOV
191
                                } else if ("POST".equals(httpRequest.getMethod())) {
×
192
                                        // only clear errors before POSTS so that redirects can show errors too.
UNCOV
193
                                        errors.clear();
×
UNCOV
194
                                        msgs.clear();
×
UNCOV
195
                                        doPost(httpRequest, httpResponse);
×
196
                                }
197
                        }
198
                        // Don't continue down the filter chain otherwise Spring complains
199
                        // that it hasn't been set up yet.
200
                        // The jsp and servlet filter are also on this chain, so writing to
201
                        // the response directly here is the only option
202
                }
203
        }
1✔
204
        
205
        /**
206
         * Convenience method to set up the velocity context properly
207
         */
208
        private void initializeVelocity() {
UNCOV
209
                if (velocityEngine == null) {
×
210
                        velocityEngine = new VelocityEngine();
×
211
                        
UNCOV
212
                        Properties props = new Properties();
×
213
                        props.setProperty(RuntimeConstants.RUNTIME_LOG, "startup_wizard_vel.log");
×
214
                        // Linux requires setting logging properties to initialize Velocity Context.
215
                        props.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
×
216
                            "org.apache.velocity.runtime.log.CommonsLogLogChute");
UNCOV
217
                        props.setProperty(CommonsLogLogChute.LOGCHUTE_COMMONS_LOG_NAME, "initial_wizard_velocity");
×
218
                        
219
                        // so the vm pages can import the header/footer
UNCOV
220
                        props.setProperty(RuntimeConstants.RESOURCE_LOADER, "class");
×
221
                        props.setProperty("class.resource.loader.description", "Velocity Classpath Resource Loader");
×
222
                        props.setProperty("class.resource.loader.class",
×
223
                            "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
224
                        
225
                        try {
UNCOV
226
                                velocityEngine.init(props);
×
227
                        }
UNCOV
228
                        catch (Exception e) {
×
UNCOV
229
                                log.error("velocity init failed, because: {}", e, e);
×
UNCOV
230
                        }
×
231
                }
UNCOV
232
        }
×
233
        
234
        /**
235
         * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on GET requests
236
         *
237
         * @param httpRequest
238
         * @param httpResponse
239
         */
240
        protected abstract void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
241
                throws IOException, ServletException;
242
        
243
        /**
244
         * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on POST requests
245
         *
246
         * @param httpRequest
247
         * @param httpResponse
248
         */
249
        protected abstract void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
250
                throws IOException, ServletException;
251
        
252
        /**
253
         * All private attributes on this class are returned to the template via the velocity context and
254
         * reflection
255
         *
256
         * @param templateName the name of the velocity file to render. This name is prepended with
257
         *            {@link #getTemplatePrefix()}
258
         * @param referenceMap
259
         * @param httpResponse
260
         */
261
        protected void renderTemplate(String templateName, Map<String, Object> referenceMap, HttpServletResponse httpResponse)
262
                throws IOException {
263
                // first we should get velocity tools context for current client request (within
264
                // his http session) and merge that tools context with basic velocity context
265
                if (referenceMap == null) {
×
UNCOV
266
                        return;
×
267
                }
268
                
269
                Object locale = referenceMap.get(FilterUtil.LOCALE_ATTRIBUTE);
×
UNCOV
270
                ToolContext velocityToolContext = getToolContext(
×
271
                    locale != null ? locale.toString() : Context.getLocale().toString());
×
UNCOV
272
                VelocityContext velocityContext = new VelocityContext(velocityToolContext);
×
273
                
274
                for (Map.Entry<String, Object> entry : referenceMap.entrySet()) {
×
UNCOV
275
                        velocityContext.put(entry.getKey(), entry.getValue());
×
276
                }
×
277
                
UNCOV
278
                Object model = getUpdateFilterModel();
×
279
                
280
                // put each of the private varibles into the template for convenience
281
                for (Field field : model.getClass().getDeclaredFields()) {
×
282
                        try {
UNCOV
283
                                field.setAccessible(true);
×
284
                                velocityContext.put(field.getName(), field.get(model));
×
285
                        }
286
                        catch (IllegalArgumentException | IllegalAccessException e) {
×
287
                                log.error("Error generated while getting field value: " + field.getName(), e);
×
UNCOV
288
                        }
×
289
                }
290
                
291
                String fullTemplatePath = getTemplatePrefix() + templateName;
×
UNCOV
292
                InputStream templateInputStream = getClass().getClassLoader().getResourceAsStream(fullTemplatePath);
×
UNCOV
293
                if (templateInputStream == null) {
×
294
                        throw new IOException("Unable to find " + fullTemplatePath);
×
295
                }
296
                
297
                velocityContext.put("errors", errors);
×
UNCOV
298
                velocityContext.put("msgs", msgs);
×
299
                
300
                // explicitly set the content type for the response because some servlet containers are assuming text/plain
301
                httpResponse.setContentType("text/html");
×
302
                
303
                try {
UNCOV
304
                        velocityEngine.evaluate(velocityContext, httpResponse.getWriter(), this.getClass().getName(),
×
305
                            new InputStreamReader(templateInputStream, StandardCharsets.UTF_8));
306
                }
UNCOV
307
                catch (Exception e) {
×
UNCOV
308
                        throw new APIException("Unable to process template: " + fullTemplatePath, e);
×
UNCOV
309
                }
×
310
        }
×
311
        
312
        /**
313
         * Makes a request to the root of the application to trigger setup.
314
         *
315
         * @param request the incoming request
316
         */
317
        private void triggerSetup(HttpServletRequest request) {
318
                try {
319
                        String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
×
UNCOV
320
                                + request.getContextPath() + "/";
×
UNCOV
321
                        HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
×
UNCOV
322
                        con.setRequestMethod("GET");
×
UNCOV
323
                        con.setConnectTimeout(2000); // 2 seconds
×
UNCOV
324
                        con.setReadTimeout(2000);
×
UNCOV
325
                        con.setInstanceFollowRedirects(true);
×
326
                        
UNCOV
327
                        con.getResponseCode();
×
328
                }
UNCOV
329
                catch (IOException e) {
×
UNCOV
330
                        log.error("Health check probe to root path failed", e);
×
UNCOV
331
                }
×
UNCOV
332
        }
×
333
        
334
        /**
335
         * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
336
         */
337
        @Override
338
        public void init(FilterConfig filterConfig) throws ServletException {
UNCOV
339
                this.filterConfig = filterConfig;
×
UNCOV
340
                initializeVelocity();
×
UNCOV
341
        }
×
342
        
343
        /**
344
         * @see javax.servlet.Filter#destroy()
345
         */
346
        @Override
347
        public void destroy() {
UNCOV
348
        }
×
349
        
350
        /**
351
         * This string is prepended to all templateNames passed to
352
         * {@link #renderTemplate(String, Map, HttpServletResponse)}
353
         *
354
         * @return string to prepend as the path for the templates
355
         */
356
        protected String getTemplatePrefix() {
357
                return "org/openmrs/web/filter/";
×
358
        }
359
        
360
        /**
361
         * The model that is used as the backer for all pages in this startup wizard. Should never return
362
         * null.
363
         *
364
         * @return the stored formbacking/model object
365
         */
366
        protected abstract Object getUpdateFilterModel();
367
        
368
        /**
369
         * If this returns true, this filter fails early and quickly. All logic is skipped and startup and
370
         * usage continue normally.
371
         *
372
         * @return true if this filter can be skipped
373
         */
374
        public abstract boolean skipFilter(HttpServletRequest request);
375

376
        /**
377
         * Convenience method to read the last 5 log lines from the MemoryAppender
378
         * 
379
         * The log lines will be added to the "logLines" key
380
         * 
381
         * @param result A map to be returned as a JSON document
382
         */
383
        protected void addLogLinesToResponse(Map<String, Object> result) {
384
                MemoryAppender appender = OpenmrsLoggingUtil.getMemoryAppender();
×
385
                if (appender != null) {
×
UNCOV
386
                        List<String> logLines = appender.getLogLines();
×
387
                        
388
                        // truncate the list to the last five so we don't overwhelm jquery
UNCOV
389
                        if (logLines.size() > 5) {
×
UNCOV
390
                                logLines = logLines.subList(logLines.size() - 5, logLines.size());
×
391
                        }
392
                        
UNCOV
393
                        result.put("logLines", logLines);
×
UNCOV
394
                } else {
×
UNCOV
395
                        result.put("logLines", Collections.emptyList());
×
396
                }
UNCOV
397
        }
×
398
        
399
        /**
400
         * Convenience method to convert the given object to a JSON string. Supports Maps, Lists, Strings,
401
         * Boolean, Double
402
         *
403
         * @param object object to convert to json
404
         * @return JSON string to be eval'd in javascript
405
         */
406
        protected String toJSONString(Object object) {
407
                ObjectMapper mapper = new ObjectMapper();
×
408
                mapper.getFactory().setCharacterEscapes(new OpenmrsCharacterEscapes());
×
409
                try {
UNCOV
410
                        return mapper.writeValueAsString(object);
×
411
                }
412
                catch (IOException e) {
×
UNCOV
413
                        log.error("Failed to convert object to JSON");
×
UNCOV
414
                        throw new APIException(e);
×
415
                }
416
        }
417
        
418
        /**
419
         * Gets tool context for specified locale parameter. If context does not exists, it creates new
420
         * context, configured for that locale. Otherwise, it changes locale property of
421
         * {@link LocalizationTool} object, that is being contained in tools context
422
         *
423
         * @param locale the string with locale parameter for configuring tools context
424
         * @return the tool context object
425
         */
426
        public ToolContext getToolContext(String locale) {
UNCOV
427
                Locale systemLocale = LocaleUtility.fromSpecification(locale);
×
428
                //Defaults to en if systemLocale is null or invalid e.g en_GBs
UNCOV
429
                if (systemLocale == null || !ArrayUtils.contains(Locale.getAvailableLocales(), systemLocale)) {
×
UNCOV
430
                        systemLocale = Locale.ENGLISH;
×
431
                }
432
                // If tool context has not been configured yet
UNCOV
433
                if (toolContext == null) {
×
434
                        // first we are creating manager for tools, factory for configuring tools 
435
                        // and empty configuration object for velocity tool box
436
                        ToolManager velocityToolManager = new ToolManager();
×
UNCOV
437
                        FactoryConfiguration factoryConfig = new FactoryConfiguration();
×
438
                        // since we are using one tool box for all request within wizard
439
                        // we should propagate toolbox's scope on all application 
UNCOV
440
                        ToolboxConfiguration toolbox = new ToolboxConfiguration();
×
UNCOV
441
                        toolbox.setScope(Scope.APPLICATION);
×
442
                        // next we are directly configuring custom localization tool by
443
                        // setting its class name, locale property etc.
UNCOV
444
                        ToolConfiguration localizationTool = new ToolConfiguration();
×
UNCOV
445
                        localizationTool.setClassname(LocalizationTool.class.getName());
×
UNCOV
446
                        localizationTool.setProperty(ToolContext.LOCALE_KEY, systemLocale);
×
UNCOV
447
                        localizationTool.setProperty(LocalizationTool.BUNDLES_KEY, "messages");
×
448
                        // and finally we are adding just configured tool into toolbox
449
                        // and creating tool context for this toolbox
UNCOV
450
                        toolbox.addTool(localizationTool);
×
UNCOV
451
                        factoryConfig.addToolbox(toolbox);
×
UNCOV
452
                        velocityToolManager.configure(factoryConfig);
×
UNCOV
453
                        toolContext = velocityToolManager.createContext();
×
UNCOV
454
                        toolContext.setUserCanOverwriteTools(true);
×
UNCOV
455
                } else {
×
456
                        // if it already has been configured, we just pull out our custom localization tool 
457
                        // from tool context, then changing its locale property and putting this tool back to the context
458
                        // First, we need to obtain the value of default key annotation of our localization tool
459
                        // class using reflection
UNCOV
460
                        DefaultKey annotation = LocalizationTool.class.getAnnotation(DefaultKey.class);
×
UNCOV
461
                        String key = annotation.value();
×
462
                        //
UNCOV
463
                        LocalizationTool localizationTool = (LocalizationTool) toolContext.get(key);
×
UNCOV
464
                        localizationTool.setLocale(systemLocale);
×
UNCOV
465
                        toolContext.put(key, localizationTool);
×
466
                }
UNCOV
467
                return toolContext;
×
468
        }
469
}
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