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