Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Front controller fixes #1098

Merged
merged 5 commits into from
Dec 24, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,21 +32,36 @@
* <br>
* 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<String> supportedLanguages;

private AcceptHeaderLocaleResolver localeResolver;

private boolean staticOnly;

private Pattern localeBundlePrefixPattern = Pattern.compile("^[a-z]{2}/.*");

Check warning on line 51 in src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java#L51

Added line #L51 was not covered by tests

public FrontControllerResolver(
ConfigurationRepository configurationRepository,
ApplicationRepository applicationRepository,
List<String> 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) {

Check warning on line 57 in src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java#L57

Added line #L57 was not covered by tests
this.configurationRepository = configurationRepository;
this.applicationRepository = applicationRepository;
this.staticOnly = staticOnly;
}

Check warning on line 60 in src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java#L60

Added line #L60 was not covered by tests

@Override
public void afterPropertiesSet() {
this.staticOnly = activeProfile.contains("static-only");

Check warning on line 64 in src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java#L64

Added line #L64 was not covered by tests

this.localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setSupportedLocales(supportedLanguages.stream().map(Locale::new).toList());
Expand All @@ -64,8 +85,13 @@
}

// 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);

Check warning on line 88 in src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java#L88

Added line #L88 was not covered by tests
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);

Check warning on line 94 in src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/FrontControllerResolver.java#L94

Added line #L94 was not covered by tests
}

// When the request path denotes an app, return the index.html for the default language
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Check warning on line 22 in src/main/java/org/tailormap/api/configuration/base/IndexHtmlTransformer.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/IndexHtmlTransformer.java#L22

Added line #L22 was not covered by tests
@Value("${tailormap-api.sentry.dsn}")
private String sentryDsn;

@Override
@NonNull
Expand All @@ -43,22 +30,14 @@
@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);

Check warning on line 40 in src/main/java/org/tailormap/api/configuration/base/IndexHtmlTransformer.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/IndexHtmlTransformer.java#L39-L40

Added lines #L39 - L40 were not covered by tests
return new TransformedResource(resource, html.getBytes(UTF_8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> 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;

Check warning on line 32 in src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java#L31-L32

Added lines #L31 - L32 were not covered by tests
this.indexHtmlTransformer = indexHtmlTransformer;
this.configurationRepository = configurationRepository;
this.applicationRepository = applicationRepository;
}

@Override
Expand All @@ -55,20 +39,23 @@
.addResourceHandler("/version.json")
.addResourceLocations(resourceLocations.split(",", -1)[0])
.setCacheControl(CacheControl.noStore());
registry

Check warning on line 42 in src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java#L42

Added line #L42 was not covered by tests
// 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());

Check warning on line 48 in src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java#L44-L48

Added lines #L44 - L48 were not covered by tests
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 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)

Check warning on line 58 in src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/tailormap/api/configuration/base/WebMvcConfig.java#L57-L58

Added lines #L57 - L58 were not covered by tests
.addResolver(new EncodedResourceResolver())
.addTransformer(indexHtmlTransformer);
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading