Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ cd openmrs-module-spa && mvn clean install
```

## Configurations
| Property | Description | Default Value |
| ----------- | ----------- | ------------ |
| `spa.local.directory` | The directory containing the Frontend 3.0 application's `index.html`. Can be an absolute path, or relative to the application data directory. Only used if `spa.remote.enabled` is false. | frontend |
| `spa.remote.enabled` | If enabled, serves from `spa.remote.url` instead of `spa.local.directory` | false |
| `spa.remote.url` | The URL of the Frontend 3.0 application files. Only used if `spa.remote.enabled` is true. | https://spa-modules.nyc3.digitaloceanspaces.com/@openmrs/esm-app-shell/latest/ |
| Property | Description | Default Value |
|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| `spa.local.directory` | The directory containing the Frontend 3.0 application's `index.html`. Can be an absolute path, or relative to the application data directory. Only used if `spa.remote.enabled` is false. | frontend |
| `spa.remote.enabled` | If enabled, serves from `spa.remote.url` instead of `spa.local.directory` | false |
| `spa.remote.url` | The URL of the Frontend 3.0 application files. Only used if `spa.remote.enabled` is true. | https://spa-modules.nyc3.digitaloceanspaces.com/@openmrs/esm-app-shell/latest/ |

Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ private SpaConstants() {}
public static final String GP_IS_REMOTE_ENABLED = "spa.remote.enabled";

public static final String GP_REMOTE_URL = "spa.remote.url";

}

123 changes: 117 additions & 6 deletions omod/src/main/java/org/openmrs/module/spa/servlet/SpaServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.openmrs.User;
import org.openmrs.api.context.Context;
import org.openmrs.module.ModuleException;
import org.openmrs.module.spa.component.ResourceLoaderComponent;
import org.openmrs.module.spa.utils.SpaModuleUtils;
import org.openmrs.util.OpenmrsUtil;
Expand All @@ -21,10 +26,15 @@
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;

@Slf4j
public class SpaServlet extends HttpServlet {
Expand All @@ -33,6 +43,8 @@ public class SpaServlet extends HttpServlet {

private static final String BASE_URL = "/spa/spaServlet";

private static final String JSON_CONFIG_FILE_NAME = "config.json";

/**
* Used for caching purposes
*
Expand Down Expand Up @@ -62,6 +74,81 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t
}
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
String requestURI = request.getRequestURI();
if (!requestURI.endsWith("/config.json")) {
return;
}
String basicAuth = !Context.isAuthenticated() ? request.getHeader("Authorization") : null;
if (basicAuth != null && isValidAuthFormat(response, basicAuth)) {
return;
}
User user = Context.getAuthenticatedUser();
if (user == null || !user.isSuperUser()) {
log.error("Authorization error while creating a config.json file");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
saveJsonConfigFile(request, response);
}

private void saveJsonConfigFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
File jsonConfigFile = getJsonConfigFile();
try {
BufferedReader reader = request.getReader();
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
String requestBody = stringBuilder.toString();

new ObjectMapper().readTree(requestBody); // verify that is in a valid JSON format

InputStream inputStream = new ByteArrayInputStream(requestBody.getBytes(StandardCharsets.UTF_8));
OutputStream outStream = Files.newOutputStream(jsonConfigFile.toPath());
OpenmrsUtil.copyFile(inputStream, outStream);

if (jsonConfigFile.exists()) {
log.debug("file: '{}' written successfully", jsonConfigFile.getAbsolutePath());
response.setStatus(HttpServletResponse.SC_OK);
}
} catch (JsonProcessingException e) {
log.error("Invalid JSON format", e);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
}

private boolean isValidAuthFormat(HttpServletResponse response, String basicAuth) {
if (basicAuth.startsWith("Basic")) {
try {
// remove the leading "Basic "
basicAuth = basicAuth.substring(6);
if (StringUtils.isBlank(basicAuth)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid credentials provided");
return true;
}

String decoded = new String(Base64.getDecoder().decode(basicAuth), StandardCharsets.UTF_8);
if (StringUtils.isBlank(decoded) || !decoded.contains(":")) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid credentials provided");
return true;
}

String[] userAndPass = decoded.split(":");
Context.authenticate(userAndPass[0], userAndPass[1]);
log.debug("authenticated [{}]", userAndPass[0]);
}
catch (Exception ex) {
// This filter never stops execution. If the user failed to
// authenticate, that will be caught later.
log.debug("authentication exception ", ex);
}
}
return false;
}

protected void handleLocalAssets(HttpServletRequest request, HttpServletResponse response) throws IOException {
File file = getFile(request);

Expand All @@ -70,13 +157,16 @@ protected void handleLocalAssets(HttpServletRequest request, HttpServletResponse
return;
}

if (file.getAbsolutePath().endsWith("/config.json")) {
response.setContentType("application/json;charset=UTF-8");
}
response.setDateHeader("Last-Modified", file.lastModified());
addCacheControlHeader(request, response);
response.setContentLength((int) file.length());
String mimeType = getServletContext().getMimeType(file.getName());
response.setContentType(mimeType);

try (InputStream is = new FileInputStream(file)) {
try (InputStream is = Files.newInputStream(file.toPath())) {
OpenmrsUtil.copyFile(is, response.getOutputStream());
}
}
Expand All @@ -89,12 +179,15 @@ protected void handleLocalAssets(HttpServletRequest request, HttpServletResponse
* @param response {@link HttpServletResponse}
* @throws IOException {@link IOException} F
*/
protected void handleRemoteAssets(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
protected void handleRemoteAssets(HttpServletRequest request, HttpServletResponse response) throws IOException {
Resource resource = getResource(request);
if (!resource.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (resource.getFilename().endsWith("/config.json")) {
response.setContentType("application/json;charset=UTF-8");
}
response.setDateHeader("Last-Modified", resource.lastModified());
addCacheControlHeader(request, response);
response.setContentLength((int) resource.contentLength());
Expand All @@ -117,6 +210,10 @@ protected void handleRemoteAssets(HttpServletRequest request, HttpServletRespons
*/
protected Resource getResource(HttpServletRequest request) {
String path = request.getPathInfo();
if (path == null) { //dynamically defined servlets see https://wiki.openmrs.org/display/docs/Module+Servlets
String url = String.valueOf(request.getRequestURL());
path = url.substring(url.lastIndexOf('/') + 1);
}
/*
* we want to extract everything after /spa/spaServlet from the path info.
* This should cater for sub-directories
Expand Down Expand Up @@ -154,8 +251,14 @@ protected File getFile(HttpServletRequest request) {
// all url will have a base of /spa/spaResources/
String path = request.getPathInfo();

// we want to extract everything after /spa/spaResources/ from the path info. This should cater for sub-directories
String extractedFile = path.substring(path.indexOf('/', BASE_URL.length() - 1) + 1);
String extractedFile = "";
if (path == null) { //dynamically defined servlets see https://wiki.openmrs.org/display/docs/Module+Servlets
String url = String.valueOf(request.getRequestURL());
extractedFile = url.substring(url.lastIndexOf('/') + 1);
} else {
// we want to extract everything after /spa/spaResources/ from the path info. This should cater for sub-directories
extractedFile = path.substring(path.indexOf('/', BASE_URL.length() - 1) + 1);
}
File folder = SpaModuleUtils.getSpaStaticFilesDir();

//Resolve default index.html
Expand All @@ -173,9 +276,17 @@ protected File getFile(HttpServletRequest request) {

private void addCacheControlHeader(HttpServletRequest request, HttpServletResponse response) {
String path = request.getPathInfo();
if (path.endsWith("importmap.json") || path.endsWith("import-map.json")) {
if (path != null && (path.endsWith("importmap.json") || path.endsWith("import-map.json"))) {
response.setHeader("Cache-Control", "public, must-revalidate, max-age=0;");
}
}

private File getJsonConfigFile() {
File folder = SpaModuleUtils.getSpaStaticFilesDir();
if (!folder.isDirectory()) {
throw new ModuleException("SPA frontend repository is not a directory at: " + folder.getAbsolutePath());
}
return new File(folder.getAbsolutePath(), JSON_CONFIG_FILE_NAME);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.spa.web;

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

import org.openmrs.module.spa.servlet.SpaServlet;
import org.springframework.stereotype.Component;
import org.springframework.web.context.ServletContextAware;

@Component
public class SpaWebComponentRegistrar implements ServletContextAware {

@Override
public void setServletContext(ServletContext servletContext) {

try {
ServletRegistration servletReg = servletContext.addServlet("spaServlet", new SpaServlet());
servletReg.addMapping("/ws/frontend/config.json");
}
catch (Exception ex) {
//TODO need a work around for: java.lang.IllegalStateException: Started
//Unable to configure mapping for servlet because this servlet context has already been initialized.
//This happens on running openmrs after InitializationFilter or UpdateFilter
//hence requiring a restart to see any page other than index.htm
//After a restart, all mappings will then happen within Listener.contextInitialized()
ex.printStackTrace();
}
}
}
Empty file added openmrs.log
Empty file.