Skip to content

Commit

Permalink
Rewrite the front controller to use a ResourceResolver so not all pos…
Browse files Browse the repository at this point in the history
…sible routes need to be mapped
  • Loading branch information
matthijsln committed Dec 20, 2024
1 parent 7cbf798 commit 5160495
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 129 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2024 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/

package org.tailormap.api.configuration.base;

import jakarta.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.resource.ResourceResolver;
import org.springframework.web.servlet.resource.ResourceResolverChain;
import org.springframework.web.util.UriUtils;
import org.tailormap.api.persistence.Application;
import org.tailormap.api.persistence.Configuration;
import org.tailormap.api.repository.ApplicationRepository;
import org.tailormap.api.repository.ConfigurationRepository;

/**
* Resolver which returns index.html for requests to paths created by the Angular routing module.
* <br>
* When the user refreshes the page such routes are requested from the server.
*/
public class FrontControllerResolver implements ResourceResolver {
// Hardcoded list for now. In the future scan the spring.web.resources.static-locations directory
// for subdirectories of locale-specific frontend bundles.
private static final AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();

static {
localeResolver.setSupportedLocales(
List.of(new Locale("en"), new Locale("nl"), new Locale("de")));
localeResolver.setDefaultLocale(localeResolver.getSupportedLocales().get(0));
}

private final ConfigurationRepository configurationRepository;
private final ApplicationRepository applicationRepository;
private final boolean staticOnly;

public FrontControllerResolver(
ConfigurationRepository configurationRepository,
ApplicationRepository applicationRepository,
boolean staticOnly) {
this.configurationRepository = configurationRepository;
this.applicationRepository = applicationRepository;
this.staticOnly = staticOnly;
}

@Override
public Resource resolveResource(
HttpServletRequest request,
@NonNull String requestPath,
@NonNull List<? extends Resource> locations,
ResourceResolverChain chain) {
// Front controller logic: when routes used by the frontend are directly requested, return the
// index.html instead of a 404.
// Paths in @RequestMapping have higher priority than this resolver

// When the resource exists (such as HTML, CSS, JS, etc.), return it
Resource resource = chain.resolveResource(request, requestPath, locations);
if (resource != null) {
return resource;
}

// Check if the request path already starts with a locale prefix like en/ or nl/
if (requestPath.matches("^[a-z]{2}/.*")) {
return chain.resolveResource(request, requestPath.substring(0, 2) + "/index.html", locations);
}

// When the request path denotes an app, return the index.html for the default language
// configured for the app

if (!staticOnly) {
Application app = null;
if ("index.html".equals(requestPath) || requestPath.matches("^app/?")) {
String defaultAppName = configurationRepository.get(Configuration.DEFAULT_APP);
app = applicationRepository.findByName(defaultAppName);
} else if (requestPath.startsWith("app/")) {
String[] parts = requestPath.split("/", -1);
if (parts.length > 1) {
String appName = UriUtils.decode(parts[1], StandardCharsets.UTF_8);
app = applicationRepository.findByName(appName);
}
}

if (app != null && app.getSettings().getI18nSettings() != null) {
String appLanguage = app.getSettings().getI18nSettings().getDefaultLanguage();
if (appLanguage != null
&& localeResolver.getSupportedLocales().stream()
.anyMatch(l -> l.toLanguageTag().equals(appLanguage))) {
return chain.resolveResource(request, appLanguage + "/index.html", locations);
}
}
}

// Otherwise use the LocaleResolver to return the index.html for the language of the
// Accept-Language header
Locale locale = localeResolver.resolveLocale(request);
return chain.resolveResource(request, locale.toLanguageTag() + "/index.html", locations);
}

@Override
public String resolveUrlPath(
@NonNull String resourcePath,
@NonNull List<? extends Resource> locations,
ResourceResolverChain chain) {
return chain.resolveUrlPath(resourcePath, locations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ public Resource transform(
throws IOException {
// Note that caching is not required because of cacheResources param to resourceChain() in
// WebMvcConfig

resource = transformerChain.transform(request, resource);

if (!"index.html".equals(resource.getFilename())) {
return resource;
}

String html = IOUtils.toString(resource.getInputStream(), UTF_8);
String sentryDsn = environment.getProperty("VIEWER_SENTRY_DSN");
if (isNotBlank(sentryDsn)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.CacheControl;
import org.springframework.lang.NonNull;
Expand All @@ -16,6 +17,8 @@
import org.springframework.web.servlet.resource.EncodedResourceResolver;
import org.tailormap.api.configuration.CaseInsensitiveEnumConverter;
import org.tailormap.api.persistence.json.GeoServiceProtocol;
import org.tailormap.api.repository.ApplicationRepository;
import org.tailormap.api.repository.ConfigurationRepository;
import org.tailormap.api.scheduling.TaskType;

@Configuration
Expand All @@ -25,30 +28,44 @@ public class WebMvcConfig implements WebMvcConfigurer {
@Value("${spring.web.resources.static-locations:file:/home/spring/static/}")
private String resourceLocations;

public WebMvcConfig(IndexHtmlTransformer indexHtmlTransformer) {
@Value("${spring.profiles.active:}")
private String activeProfile;

private final ConfigurationRepository configurationRepository;
private final ApplicationRepository applicationRepository;

public WebMvcConfig(
IndexHtmlTransformer indexHtmlTransformer,
// Inject these repositories lazily because in the static-only profile these are not needed
// but also not configured
@Lazy ConfigurationRepository configurationRepository,
@Lazy ApplicationRepository applicationRepository) {
this.indexHtmlTransformer = indexHtmlTransformer;
this.configurationRepository = configurationRepository;
this.applicationRepository = applicationRepository;
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/*/index.html")
.addResourceLocations(resourceLocations.split(",", -1)[0])
// no-cache means the browser must revalidate index.html with a conditional HTTP request
// using If-Modified-Since. This is needed to always have the latest frontend loaded in the
// browser after deployment of a new release.
.setCacheControl(CacheControl.noCache())
.resourceChain(true)
.addTransformer(indexHtmlTransformer);
registry
.addResourceHandler("/version.json")
.addResourceLocations(resourceLocations.split(",", -1)[0])
.setCacheControl(CacheControl.noStore());
registry
.addResourceHandler("/**")
.addResourceLocations(resourceLocations.split(",", -1)[0])
// no-cache means the browser must revalidate index.html with a conditional HTTP request
// using If-Modified-Since. This is needed to always have the latest frontend loaded in the
// browser after deployment of a new release.
.setCacheControl(CacheControl.noCache())
.resourceChain(true)
.addResolver(new EncodedResourceResolver());
.addResolver(
new FrontControllerResolver(
configurationRepository,
applicationRepository,
activeProfile.contains("static-only")))
.addResolver(new EncodedResourceResolver())
.addTransformer(indexHtmlTransformer);
}

@Override
Expand Down

0 comments on commit 5160495

Please sign in to comment.