com.xlrit.gears.server.frontend.FrontendController Maven / Gradle / Ivy
The newest version!
package com.xlrit.gears.server.frontend;
import java.util.Objects;
import com.xlrit.gears.server.security.AuthProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Preconditions;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import static java.util.Objects.requireNonNullElse;
import static org.springframework.http.MediaType.*;
@Controller
public class FrontendController {
private static final Logger LOG = LoggerFactory.getLogger(FrontendController.class);
private static final String REACT_PREFIX = "REACT_APP_";
private static final String IMAGE_SVG_VALUE = "image/svg+xml";
private static final MediaType IMAGE_SVG = MediaType.valueOf(IMAGE_SVG_VALUE);
private final ObjectMapper objectMapper;
private final ObjectNode configNode;
public FrontendController(
FrontendProperties frontendProperties,
AuthProperties authProperties,
ObjectMapper objectMapper
) {
this.objectMapper = objectMapper;
this.configNode = objectMapper.createObjectNode();
copy(configNode, "TITLE", requireNonNullElse(frontendProperties.getTitle(), frontendProperties.getName())); // "name" was renamed to "title"
copy(configNode, "THEME", frontendProperties.getTheme());
copy(configNode, "ROUTER_MODE", frontendProperties.getRouterMode());
copy(configNode, "ENABLE_HTML", frontendProperties.isEnableHtml());
copy(configNode, "LANGS_ENABLED", frontendProperties.getLanguagesAsString());
copy(configNode, "AUTH_MODE", authProperties.getMode());
if ("external".equals(authProperties.getMode())) {
copy(configNode, "AUTH_CLIENT_ID", authProperties.getExternal().getClientId());
copy(configNode, "AUTH_AUTHORITY", authProperties.getExternal().getAuthority());
copy(configNode, "AUTH_REDIRECT_URI", authProperties.getExternal().getRedirectUri());
copy(configNode, "AUTH_CACHE_LOCATION", authProperties.getExternal().getCacheLocation());
}
if (frontendProperties.getAdditional() != null) {
frontendProperties.getAdditional().forEach((name, value) -> {
copy(configNode, name, value);
});
}
LOG.info("Frontend configuration: {}", configNode);
}
// see https://stackoverflow.com/questions/47689971/how-to-work-with-react-routers-and-spring-boot-controller
@RequestMapping(value = { "/", "/gears", "/gears/**" })
public String getIndex(HttpServletRequest request) {
LOG.debug("getIndex: requestURI={}", request.getRequestURI());
return "/index.html";
}
@GetMapping(value = "/config.json", produces = "application/json")
@ResponseBody
public JsonNode configJson() {
return configNode;
}
@GetMapping(value = "/config.js", produces = "text/javascript")
@ResponseBody
public String configJs() throws JsonProcessingException {
return "window._env=" + objectMapper.writeValueAsString(configNode);
}
@GetMapping(value = "/menu.json", produces = "application/json")
@ResponseBody
public Resource menuJson() {
return new ClassPathResource("menu.json");
}
@GetMapping(value = "/theme.json", produces = "application/json")
@ResponseBody
public Resource themeJson() {
return new ClassPathResource("theme.json");
}
@GetMapping(value = "/logos/{filename}", produces = { IMAGE_JPEG_VALUE, IMAGE_PNG_VALUE, IMAGE_SVG_VALUE })
@ResponseBody
public ResponseEntity logo(@PathVariable String filename) {
Preconditions.checkNotNull(filename, "Filename is required");
Preconditions.checkArgument(!filename.contains("/"), "Invalid filename");
ClassPathResource resource = new ClassPathResource("logos/" + filename);
return ResponseEntity.ok()
.contentType(deriveMediaType(filename))
.body(resource);
}
@GetMapping(value = "/icons/{filename}", produces = { IMAGE_JPEG_VALUE, IMAGE_PNG_VALUE, IMAGE_SVG_VALUE })
@ResponseBody
public ResponseEntity icon(@PathVariable String filename) {
Preconditions.checkNotNull(filename, "Filename is required");
Preconditions.checkArgument(!filename.contains("/"), "Invalid filename");
ClassPathResource resource = new ClassPathResource("icons/" + filename);
return ResponseEntity.ok()
.contentType(deriveMediaType(filename))
.body(resource);
}
private MediaType deriveMediaType(String filename) {
String extension = StringUtils.getFilenameExtension(filename.toLowerCase());
return switch (Objects.requireNonNull(extension)) {
case "jpg" -> IMAGE_JPEG;
case "png" -> IMAGE_PNG;
case "svg" -> IMAGE_SVG;
default -> throw new IllegalArgumentException("Unsupported extension '" + extension + "'");
};
}
private static void copy(ObjectNode configNode, String key, String value) {
if (value != null) configNode.put(REACT_PREFIX + key, value);
}
private static void copy(ObjectNode configNode, String key, boolean value) {
// the Frontend expects all values to be strings
configNode.put(REACT_PREFIX + key, String.valueOf(value));
}
}