diff --git a/src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java b/src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java
index bc733b66c..9c7862823 100644
--- a/src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java
+++ b/src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java
@@ -10,8 +10,14 @@
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Component;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.resource.ResourceResolver;
import org.springframework.web.servlet.resource.ResourceResolverChain;
@@ -26,21 +32,36 @@
*
* When the user refreshes the page such routes are requested from the server.
*/
-public class FrontControllerResolver implements ResourceResolver {
- private final AcceptHeaderLocaleResolver localeResolver;
+@Component
+public class FrontControllerResolver implements ResourceResolver, InitializingBean {
private final ConfigurationRepository configurationRepository;
private final ApplicationRepository applicationRepository;
- private final boolean staticOnly;
+
+ @Value("${spring.profiles.active:}")
+ private String activeProfile;
+
+ @Value("#{'${tailormap-api.supported-languages:en}'.split(',')}")
+ private List supportedLanguages;
+
+ private AcceptHeaderLocaleResolver localeResolver;
+
+ private boolean staticOnly;
+
+ private Pattern localeBundlePrefixPattern = Pattern.compile("^[a-z]{2}/.*");
public FrontControllerResolver(
- ConfigurationRepository configurationRepository,
- ApplicationRepository applicationRepository,
- List supportedLanguages,
- boolean staticOnly) {
+ // 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.configurationRepository = configurationRepository;
this.applicationRepository = applicationRepository;
- this.staticOnly = staticOnly;
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+ this.staticOnly = activeProfile.contains("static-only");
this.localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setSupportedLocales(supportedLanguages.stream().map(Locale::new).toList());
@@ -64,8 +85,13 @@ public Resource resolveResource(
}
// 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);
+ String localePrefix = StringUtils.left(requestPath, 2);
+ if ((localeBundlePrefixPattern.matcher(requestPath).matches()
+ && supportedLanguages.contains(localePrefix))
+ // When the request is just "GET /nl/" or "GET /nl" the requestPath is "nl" without a
+ // trailing slash
+ || supportedLanguages.contains(requestPath)) {
+ return chain.resolveResource(request, localePrefix + "/index.html", locations);
}
// When the request path denotes an app, return the index.html for the default language
diff --git a/src/main/java/org/tailormap/api/configuration/base/IndexHtmlTransformer.java b/src/main/java/org/tailormap/api/configuration/base/IndexHtmlTransformer.java
index d043652a5..9d2e2a138 100644
--- a/src/main/java/org/tailormap/api/configuration/base/IndexHtmlTransformer.java
+++ b/src/main/java/org/tailormap/api/configuration/base/IndexHtmlTransformer.java
@@ -7,16 +7,10 @@
package org.tailormap.api.configuration.base;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.lang3.StringUtils.isNotBlank;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
-import java.lang.invoke.MethodHandles;
-import org.apache.commons.io.IOUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.context.EnvironmentAware;
-import org.springframework.core.env.Environment;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@@ -25,16 +19,9 @@
import org.springframework.web.servlet.resource.TransformedResource;
@Component
-public class IndexHtmlTransformer implements ResourceTransformer, EnvironmentAware {
- private static final Logger logger =
- LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-
- private Environment environment;
-
- @Override
- public void setEnvironment(@NonNull Environment environment) {
- this.environment = environment;
- }
+public class IndexHtmlTransformer implements ResourceTransformer {
+ @Value("${tailormap-api.sentry.dsn}")
+ private String sentryDsn;
@Override
@NonNull
@@ -43,22 +30,14 @@ public Resource transform(
@NonNull Resource resource,
@NonNull ResourceTransformerChain transformerChain)
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())) {
+ if (sentryDsn == null || !"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)) {
- logger.info(
- "Sending Sentry DSN {} for index {}", sentryDsn, resource.getFile().getAbsolutePath());
- html = html.replace("@SENTRY_DSN@", sentryDsn);
- }
+ String html = resource.getContentAsString(UTF_8);
+ html = html.replace("@SENTRY_DSN@", sentryDsn);
return new TransformedResource(resource, html.getBytes(UTF_8));
}
}
diff --git a/src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java b/src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java
index adc51425b..ad6f3fc6a 100644
--- a/src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java
+++ b/src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java
@@ -6,10 +6,9 @@
package org.tailormap.api.configuration.base;
-import java.util.List;
+import java.util.concurrent.TimeUnit;
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;
@@ -18,35 +17,20 @@
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
public class WebMvcConfig implements WebMvcConfigurer {
+ private final FrontControllerResolver frontControllerResolver;
private final IndexHtmlTransformer indexHtmlTransformer;
@Value("${spring.web.resources.static-locations:file:/home/spring/static/}")
private String resourceLocations;
- @Value("#{'${tailormap-api.supported-languages:en}'.split(',')}")
- private List supportedLanguages;
-
- @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) {
+ FrontControllerResolver frontControllerResolver, IndexHtmlTransformer indexHtmlTransformer) {
+ this.frontControllerResolver = frontControllerResolver;
this.indexHtmlTransformer = indexHtmlTransformer;
- this.configurationRepository = configurationRepository;
- this.applicationRepository = applicationRepository;
}
@Override
@@ -55,6 +39,13 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
.addResourceHandler("/version.json")
.addResourceLocations(resourceLocations.split(",", -1)[0])
.setCacheControl(CacheControl.noStore());
+ registry
+ // Add cache headers for frontend bundle resources with hash in filename and fonts/images
+ .addResourceHandler("/*/*.js", "/*/*.css", "/*/*.map", "/*/media/**", "/*/icons/**")
+ .addResourceLocations(resourceLocations.split(",", -1)[0])
+ .setCacheControl(CacheControl.maxAge(7, TimeUnit.DAYS).mustRevalidate())
+ .resourceChain(true)
+ .addResolver(new EncodedResourceResolver());
registry
.addResourceHandler("/**")
.addResourceLocations(resourceLocations.split(",", -1)[0])
@@ -62,13 +53,9 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 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 FrontControllerResolver(
- configurationRepository,
- applicationRepository,
- supportedLanguages,
- activeProfile.contains("static-only")))
+ // Don't cache resources which can vary per user because of the Accept-Language header
+ .resourceChain(false)
+ .addResolver(frontControllerResolver)
.addResolver(new EncodedResourceResolver())
.addTransformer(indexHtmlTransformer);
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index f6a281851..acf8fd221 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -7,6 +7,8 @@ tailormap-api.admin.base-path=/api/admin
# The first language is used as the default.
tailormap-api.supported-languages=en,nl,de
+tailormap-api.sentry.dsn=${VIEWER_SENTRY_DSN:#{null}}
+
tailormap-api.security.admin.create-if-not-exists=true
tailormap-api.security.admin.username=tm-admin
# A hashed password can be specified in the ADMIN_HASHED_PASSWORD environment variable. If empty or not specified a