From 7839716b7e3e793c6537f83f8b743d562e733263 Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Wed, 14 Jan 2026 14:59:27 +0100 Subject: [PATCH 1/2] refactor and improve code quality across web modules --- .../web/app/BlobStoreMustacheResolver.java | 4 +- .../web/app/DropwizardProvider.java | 2 +- .../web/app/FallbackMustacheViewRenderer.java | 3 ++ .../ii/xtraplatform/web/app/HealthPlugin.java | 2 +- .../web/app/HttpClientApache.java | 6 +-- .../ii/xtraplatform/web/app/JaxRsPlugin.java | 52 +++++++++++++++---- .../web/app/JsonProviderOptionalPretty.java | 5 +- .../web/app/LogConfigurationTask.java | 7 ++- .../ii/xtraplatform/web/app/ResourceURL.java | 14 ++--- .../xtraplatform/web/app/RobotsServlet.java | 7 +-- .../ii/xtraplatform/web/app/StaticPlugin.java | 1 + .../web/app/StaticResourceConstants.java | 2 +- ...ionExceptionCatchingFilterPreMatching.java | 2 + .../de/ii/xtraplatform/web/app/WebServer.java | 6 ++- .../xtraplatform/web/domain/JsonPretty.java | 10 ++-- 15 files changed, 81 insertions(+), 42 deletions(-) diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/BlobStoreMustacheResolver.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/BlobStoreMustacheResolver.java index 9299e4a5..1d2b2eb0 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/BlobStoreMustacheResolver.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/BlobStoreMustacheResolver.java @@ -20,7 +20,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -94,8 +93,7 @@ private Path toPath(String templateName) { private void init() { try (Stream paths = templateStore.walk(Path.of(""), 1, (path, pathAttributes) -> pathAttributes.isValue())) { - for (Iterator it = paths.iterator(); it.hasNext(); ) { - Path path = it.next(); + for (Path path : paths.toList()) { try { Optional localPath = templateStore.asLocalPath(path, false); diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/DropwizardProvider.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/DropwizardProvider.java index aeb11a3e..c8ec4539 100755 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/DropwizardProvider.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/DropwizardProvider.java @@ -73,7 +73,7 @@ public CompletionStage onStart(boolean isStartupAsync) { init(); } catch (Throwable ex) { LogContext.error(LOGGER, ex, "Error during initializing of {}", appContext.getName()); - System.exit(1); + return CompletableFuture.failedFuture(ex); } return CompletableFuture.completedFuture(null); diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/FallbackMustacheViewRenderer.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/FallbackMustacheViewRenderer.java index d3b24501..ee7e6e04 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/FallbackMustacheViewRenderer.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/FallbackMustacheViewRenderer.java @@ -49,6 +49,7 @@ public class FallbackMustacheViewRenderer extends MustacheViewRenderer implement @Inject FallbackMustacheViewRenderer(MustacheResolverRegistry mustacheResolverRegistry) { + super(); this.mustacheResolverRegistry = mustacheResolverRegistry; this.factories = CacheBuilder.newBuilder() @@ -97,7 +98,9 @@ public void configure(Map options) { useCache = Optional.ofNullable(options.get("cache")).map(Boolean::parseBoolean).orElse(true); } + // @Override @VisibleForTesting + @SuppressWarnings("PMD.MissingOverride") boolean isUseCache() { return useCache; } diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/HealthPlugin.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/HealthPlugin.java index edd2bf8e..5b7a301c 100755 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/HealthPlugin.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/HealthPlugin.java @@ -45,7 +45,7 @@ public void init(AppConfiguration configuration, Environment environment) { volatileRegistry.listen(this::register, this::unregister); } - // @Override + @Override public void register(String name, HealthCheck check) { if (!healthCheckRegistry.getNames().contains(name)) { healthCheckRegistry.register(name, check); diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/HttpClientApache.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/HttpClientApache.java index 3582e3d6..599c727f 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/HttpClientApache.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/HttpClientApache.java @@ -106,9 +106,9 @@ private static InputStream getAsInputStream(CloseableHttpClient client, HttpUriR } } - try { - CloseableHttpResponse response = client.execute(request); - return response.getEntity().getContent(); + try (CloseableHttpResponse response = client.execute(request)) { + byte[] content = EntityUtils.toByteArray(response.getEntity()); + return new java.io.ByteArrayInputStream(content); } catch (IOException e) { throw new IllegalArgumentException(e); } diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/JaxRsPlugin.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/JaxRsPlugin.java index 767811bd..19b50577 100755 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/JaxRsPlugin.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/JaxRsPlugin.java @@ -78,21 +78,31 @@ public class JaxRsPlugin implements DropwizardPlugin { // should be last @Override public int getPriority() { - return 10000; + return 10_000; } @Override public void init(AppConfiguration configuration, Environment environment) { JerseyEnvironment jersey = environment.jersey(); + setupAuthProviders(jersey); + registerEndpoints(jersey); + registerFilters(jersey); + registerBinders(jersey); + registerExceptionMappers(jersey); + processConsumers(jersey); + } + + private void setupAuthProviders(JerseyEnvironment jersey) { for (AuthProvider provider : authProviders.get()) { if (!isAuthProviderAvailable) { jersey.register(provider.getRolesAllowedDynamicFeature()); jersey.register(provider.getAuthValueFactoryProvider()); this.isAuthProviderAvailable = true; } - if (LOGGER.isDebugEnabled(MARKER.DI)) + if (LOGGER.isDebugEnabled(MARKER.DI)) { LOGGER.debug(MARKER.DI, "Registered JAX-RS Auth Provider {}", provider.getClass()); + } } if (isAuthProviderAvailable) { List filters = @@ -104,37 +114,57 @@ public void init(AppConfiguration configuration, Environment environment) { jersey.register( new WebApplicationExceptionCatchingFilterPreMatching(new ChainedAuthFilter<>(filters))); } + } + + private void registerEndpoints(JerseyEnvironment jersey) { if (isAuthProviderAvailable && !endpoints.get().isEmpty()) { for (Object resource : endpoints.get()) { jersey.register(resource); - if (LOGGER.isDebugEnabled(MARKER.DI)) + if (LOGGER.isDebugEnabled(MARKER.DI)) { LOGGER.debug(MARKER.DI, "Registered JAX-RS Resource {}", resource.getClass()); + } } - } else if (!isAuthProviderAvailable && !endpoints.get().isEmpty()) { - if (LOGGER.isDebugEnabled(MARKER.DI)) - LOGGER.debug( - MARKER.DI, "No JAX-RS Auth Provider registered yet, cannot register Resources."); + } else if (!isAuthProviderAvailable + && !endpoints.get().isEmpty() + && LOGGER.isDebugEnabled(MARKER.DI)) { + LOGGER.debug(MARKER.DI, "No JAX-RS Auth Provider registered yet, cannot register Resources."); } + } + + private void registerFilters(JerseyEnvironment jersey) { for (ContainerRequestFilter filter : containerRequestFilters.get()) { jersey.register(filter); - if (LOGGER.isDebugEnabled(MARKER.DI)) + if (LOGGER.isDebugEnabled(MARKER.DI)) { LOGGER.debug(MARKER.DI, "Registered JAX-RS ContainerRequestFilter {})", filter.getClass()); + } } for (ContainerResponseFilter filter : containerResponseFilters.get()) { jersey.register(filter); - if (LOGGER.isDebugEnabled(MARKER.DI)) + if (LOGGER.isDebugEnabled(MARKER.DI)) { LOGGER.debug(MARKER.DI, "Registered JAX-RS ContainerResponseFilter {})", filter.getClass()); + } } + } + + private void registerBinders(JerseyEnvironment jersey) { for (Binder binder : binders.get()) { jersey.register(binder); - if (LOGGER.isDebugEnabled(MARKER.DI)) + if (LOGGER.isDebugEnabled(MARKER.DI)) { LOGGER.debug(MARKER.DI, "Registered JAX-RS Binder {}", binder.getClass()); + } } + } + + private void registerExceptionMappers(JerseyEnvironment jersey) { for (ExceptionMapper exceptionMapper : exceptionMappers.get()) { jersey.register(exceptionMapper); - if (LOGGER.isDebugEnabled(MARKER.DI)) + if (LOGGER.isDebugEnabled(MARKER.DI)) { LOGGER.debug(MARKER.DI, "Registered JAX-RS ExceptionMapper {}", exceptionMapper.getClass()); + } } + } + + private void processConsumers(JerseyEnvironment jersey) { for (JaxRsConsumer consumer : consumers.get()) { if (consumer != null) { consumer diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/JsonProviderOptionalPretty.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/JsonProviderOptionalPretty.java index d90fb8ff..53ac0ffa 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/JsonProviderOptionalPretty.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/JsonProviderOptionalPretty.java @@ -19,7 +19,6 @@ import javax.ws.rs.Consumes; import javax.ws.rs.Priorities; import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.Provider; @@ -41,6 +40,7 @@ public class JsonProviderOptionalPretty extends JacksonJaxbJsonProvider private final ObjectMapper mapperPretty; public JsonProviderOptionalPretty(ObjectMapper mapper) { + super(); this.mapper = mapper; this.mapperPretty = mapper.copy().enable(SerializationFeature.INDENT_OUTPUT); @@ -48,8 +48,7 @@ public JsonProviderOptionalPretty(ObjectMapper mapper) { } @Override - public void aroundWriteTo(WriterInterceptorContext writerInterceptorContext) - throws IOException, WebApplicationException { + public void aroundWriteTo(WriterInterceptorContext writerInterceptorContext) throws IOException { // check if the request context has the JSON pretty property set if (JsonPretty.isJsonPretty(writerInterceptorContext)) { diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/LogConfigurationTask.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/LogConfigurationTask.java index 09ef254d..de180f7a 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/LogConfigurationTask.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/LogConfigurationTask.java @@ -50,7 +50,9 @@ public void init(AppConfiguration configuration, Environment environment) { @Override public void execute(Map> parameters, PrintWriter output) throws Exception { - if (LOGGER.isTraceEnabled()) LOGGER.trace("Log filter request: {}", parameters); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Log filter request: {}", parameters); + } Optional optionalThirdPartyLoggingFilter = loggerContext.getTurboFilterList().stream() @@ -66,6 +68,7 @@ public void execute(Map> parameters, PrintWriter output) th }); } + @SuppressWarnings("PMD.CyclomaticComplexity") private void setFilter(LoggingFilter loggingFilter, String filter, boolean enable) { switch (filter) { case "apiRequests": @@ -106,6 +109,8 @@ private void setFilter(LoggingFilter loggingFilter, String filter, boolean enabl loggingFilter.setConfigDumps(enable); loggingFilter.setStackTraces(enable); break; + default: + break; } } diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/ResourceURL.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/ResourceURL.java index 6356b2db..09b9bf94 100755 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/ResourceURL.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/ResourceURL.java @@ -21,7 +21,7 @@ import javax.ws.rs.NotFoundException; /** Helper methods for dealing with {@link URL} objects for local resources. */ -public class ResourceURL { +public final class ResourceURL { private ResourceURL() { /* singleton */ } @@ -59,11 +59,12 @@ public static boolean isDirectory(URL resourceURL) throws URISyntaxException { filename = filename.substring( filename.indexOf('!') + 2); // leaves just the relative file path inside the jar + @SuppressWarnings("PMD.CloseResource") final JarFile jarFile = jarConnection.getJarFile(); final ZipEntry zipEntry = jarFile.getEntry(filename); - final InputStream inputStream = jarFile.getInputStream(zipEntry); - - return (inputStream == null); + try (InputStream inputStream = jarFile.getInputStream(zipEntry)) { + return inputStream == null; + } } catch (IOException e) { throw new NotFoundException(e); } @@ -91,8 +92,8 @@ public static URL appendTrailingSlash(URL originalURL) { originalURL.getHost(), originalURL.getPort(), originalURL.getFile() + '/'); - } catch (MalformedURLException ignored) { // shouldn't happen - throw new IllegalArgumentException("Invalid resource URL: " + originalURL); + } catch (MalformedURLException e) { // shouldn't happen + throw new IllegalArgumentException("Invalid resource URL: " + originalURL, e); } } @@ -107,6 +108,7 @@ public static URL appendTrailingSlash(URL originalURL) { * @return the last modified time of the resource, expressed as the number of milliseconds since * the epoch, or 0 if there was a problem */ + @SuppressWarnings("PMD.CyclomaticComplexity") public static long getLastModified(URL resourceURL) { final String protocol = resourceURL.getProtocol(); switch (protocol) { diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/RobotsServlet.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/RobotsServlet.java index 8a56905d..2aab0184 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/RobotsServlet.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/RobotsServlet.java @@ -13,14 +13,11 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.WebApplicationException; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.StreamingOutput; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * @author zahnen @@ -45,7 +42,7 @@ @Instantiate*/ public class RobotsServlet extends HttpServlet implements ContainerResponseFilter { - private static final Logger LOGGER = LoggerFactory.getLogger(RobotsServlet.class); + private static final long serialVersionUID = 1L; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -69,7 +66,7 @@ public void filter( StreamingOutput stream = new StreamingOutput() { @Override - public void write(OutputStream output) throws IOException, WebApplicationException { + public void write(OutputStream output) throws IOException { writeContent(output); } }; diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/StaticPlugin.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/StaticPlugin.java index c8ecb317..415cfbe2 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/StaticPlugin.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/StaticPlugin.java @@ -84,6 +84,7 @@ public void init(AppConfiguration configuration, Environment environment) { } @Override + @SuppressWarnings("PMD.UselessParentheses") public boolean handle(String path, HttpServletRequest request, HttpServletResponse response) { for (String prefix : servlets.keySet()) { if (path.startsWith(prefix) || ("/" + path).startsWith(prefix)) { diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/StaticResourceConstants.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/StaticResourceConstants.java index 377443a0..cfff2ca9 100755 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/StaticResourceConstants.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/StaticResourceConstants.java @@ -38,7 +38,7 @@ public interface StaticResourceConstants { long CACHE_TIMEOUT_DISABLED = -1L; /** Default cache timeout to use. */ - long ONE_WEEK_IN_SECONDS = 604800L; + long ONE_WEEK_IN_SECONDS = 604_800L; /** Key used for the contextId */ String CONTEXTID = "ContextId"; diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/WebApplicationExceptionCatchingFilterPreMatching.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/WebApplicationExceptionCatchingFilterPreMatching.java index 6ee502f5..2cc07de8 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/WebApplicationExceptionCatchingFilterPreMatching.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/WebApplicationExceptionCatchingFilterPreMatching.java @@ -23,6 +23,8 @@ public WebApplicationExceptionCatchingFilterPreMatching(ContainerRequestFilter u this.underlying = underlying; } + @Override + @SuppressWarnings("PMD.EmptyCatchBlock") public void filter(ContainerRequestContext requestContext) throws IOException { try { this.underlying.filter(requestContext); diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/WebServer.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/WebServer.java index c1378fc8..cd162b90 100755 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/WebServer.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/app/WebServer.java @@ -60,10 +60,12 @@ public CompletionStage onStart(boolean isStartupAsync) { try { server.get().start(); - LOGGER.info("Started web server at {}", appContext.getUri()); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Started web server at {}", appContext.getUri()); + } } catch (Throwable ex) { LogContext.error(LOGGER, ex, "Error starting {}", appContext.getName()); - System.exit(1); + return CompletableFuture.failedFuture(ex); } return CompletableFuture.completedFuture(null); diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/JsonPretty.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/JsonPretty.java index 95362f07..8eb0a815 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/JsonPretty.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/JsonPretty.java @@ -19,10 +19,6 @@ public interface JsonPretty { - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) - @interface JsonPrettify {} - String JSON_PRETTY_HEADER = "x-ldproxy-json-pretty"; Annotation JSON_PRETTY_ANNOTATION = @@ -33,13 +29,17 @@ public Class annotationType() { } }; + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @interface JsonPrettify {} + static boolean isJsonPretty(WriterInterceptorContext writerInterceptorContext) { Object headerValue = writerInterceptorContext.getProperty(JSON_PRETTY_HEADER); return headerValue instanceof String && "true".equalsIgnoreCase((String) headerValue); } - static boolean isJsonPretty(Annotation[] annotations) { + static boolean isJsonPretty(Annotation... annotations) { return annotations != null && Arrays.stream(annotations).anyMatch(annotation -> annotation instanceof JsonPrettify); } From 0a083b67e8b24b92640da362524dca6b7e7c9872 Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Wed, 14 Jan 2026 15:21:14 +0100 Subject: [PATCH 2/2] refactor StaticResourceServlet and URICustomizer logic --- .../xtraplatform/web/domain/LoginHandler.java | 1 + .../web/domain/StaticResourceServlet.java | 119 +++++++++++------- .../web/domain/URICustomizer.java | 64 +++++----- 3 files changed, 111 insertions(+), 73 deletions(-) diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/LoginHandler.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/LoginHandler.java index 4c88a804..8722ed0b 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/LoginHandler.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/LoginHandler.java @@ -24,6 +24,7 @@ public interface LoginHandler { String PARAM_LOGOUT_REDIRECT_URI = "post_logout_redirect_uri"; String PARAM_LOGOUT_CLIENT_ID = "client_id"; + @SuppressWarnings("PMD.UseObjectForClearerAPI") Response handle( ContainerRequestContext containerRequestContext, String redirectUri, diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/StaticResourceServlet.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/StaticResourceServlet.java index 31297fb7..60f5989a 100755 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/StaticResourceServlet.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/StaticResourceServlet.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -29,6 +30,7 @@ public class StaticResourceServlet extends HttpServlet { + private static final long serialVersionUID = 1L; private static final Logger LOGGER = LoggerFactory.getLogger(StaticResourceServlet.class); private static final CharMatcher SLASHES = CharMatcher.is('/'); private static final String DEFAULT_PAGE = "index.html"; @@ -39,7 +41,7 @@ public class StaticResourceServlet extends HttpServlet { private final String uriPath; private final Charset defaultCharset; private final StaticResourceReader resourceReader; - private final Optional rootRedirect; + private final transient Optional rootRedirect; private final Set noCacheExtensions; /** @@ -65,6 +67,7 @@ public StaticResourceServlet( StaticResourceReader resourceReader, Set noCacheExtensions, Optional rootRedirect) { + super(); final String trimmedPath = SLASHES.trimFrom(resourcePath); this.resourcePath = trimmedPath.isEmpty() ? trimmedPath : trimmedPath + '/'; final String trimmedUri = SLASHES.trimTrailingFrom(uriPath); @@ -97,20 +100,11 @@ public StaticResourceServlet( protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { - final StringBuilder builder = new StringBuilder(); - // if (!req.getDispatcherType().equals(DispatcherType.FORWARD)) { - builder.append(req.getServletPath()); - // } - if (req.getPathInfo() != null) { - builder.append(req.getPathInfo()); - } else if (rootRedirect.isPresent()) { - builder.append(rootRedirect.get()); - resp.setHeader(HttpHeaders.LOCATION, builder.toString()); - resp.sendError(HttpServletResponse.SC_MOVED_PERMANENTLY); - return; + String assetPath = buildAssetPath(req, resp); + if (assetPath == null) { + return; // Response already sent by buildAssetPath } - String assetPath = builder.toString(); final CachedResource cachedAsset = loadAsset(assetPath); if (cachedAsset == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); @@ -122,42 +116,79 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) return; } - if (assetPath.contains(".") - && noCacheExtensions.contains(assetPath.substring(assetPath.lastIndexOf('.') + 1))) { - resp.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache"); - } + configureResponse(req, resp, assetPath, cachedAsset); + writeResponse(resp, cachedAsset); + } catch (RuntimeException | URISyntaxException ignored) { + handleException(resp, ignored); + } + } - resp.setDateHeader(HttpHeaders.LAST_MODIFIED, cachedAsset.getLastModified()); - resp.setHeader(HttpHeaders.ETAG, cachedAsset.getETag()); + private String buildAssetPath(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + final StringBuilder builder = new StringBuilder(); + builder.append(req.getServletPath()); + + if (req.getPathInfo() != null) { + builder.append(req.getPathInfo()); + } else if (rootRedirect.isPresent()) { + builder.append(rootRedirect.get()); + resp.setHeader(HttpHeaders.LOCATION, builder.toString()); + resp.sendError(HttpServletResponse.SC_MOVED_PERMANENTLY); + return null; + } - final String mimeTypeOfExtension = req.getServletContext().getMimeType(req.getRequestURI()); - MediaType mediaType = DEFAULT_MEDIA_TYPE; + return builder.toString(); + } + + private void configureResponse( + HttpServletRequest req, + HttpServletResponse resp, + String assetPath, + CachedResource cachedAsset) { + if (assetPath.contains(".") + && noCacheExtensions.contains(assetPath.substring(assetPath.lastIndexOf('.') + 1))) { + resp.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache"); + } + + resp.setDateHeader(HttpHeaders.LAST_MODIFIED, cachedAsset.getLastModified()); + resp.setHeader(HttpHeaders.ETAG, cachedAsset.getETag()); + + setContentType(req, resp); + } - if (mimeTypeOfExtension != null) { - try { - mediaType = MediaType.parse(mimeTypeOfExtension); - if (defaultCharset != null && mediaType.is(MediaType.ANY_TEXT_TYPE)) { - mediaType = mediaType.withCharset(defaultCharset); - } - } catch (IllegalArgumentException ignore) { + private void setContentType(HttpServletRequest req, HttpServletResponse resp) { + final String mimeTypeOfExtension = req.getServletContext().getMimeType(req.getRequestURI()); + MediaType mediaType = DEFAULT_MEDIA_TYPE; + + if (mimeTypeOfExtension != null) { + try { + mediaType = MediaType.parse(mimeTypeOfExtension); + if (defaultCharset != null && mediaType.is(MediaType.ANY_TEXT_TYPE)) { + mediaType = mediaType.withCharset(defaultCharset); } + } catch (IllegalArgumentException ignore) { + // Keep default media type } + } - resp.setContentType(mediaType.type() + '/' + mediaType.subtype()); + resp.setContentType(mediaType.type() + '/' + mediaType.subtype()); + if (mediaType.charset().isPresent()) { + resp.setCharacterEncoding(mediaType.charset().get().toString()); + } + } - if (mediaType.charset().isPresent()) { - resp.setCharacterEncoding(mediaType.charset().get().toString()); - } + private void writeResponse(HttpServletResponse resp, CachedResource cachedAsset) + throws IOException { + try (ServletOutputStream output = resp.getOutputStream()) { + output.write(cachedAsset.getResource()); + } + } - try (ServletOutputStream output = resp.getOutputStream()) { - output.write(cachedAsset.getResource()); - } - } catch (RuntimeException | URISyntaxException ignored) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Unexpected exception", ignored); - } - resp.sendError(HttpServletResponse.SC_NOT_FOUND); + private void handleException(HttpServletResponse resp, Exception ignored) throws IOException { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Unexpected exception", ignored); } + resp.sendError(HttpServletResponse.SC_NOT_FOUND); } private CachedResource loadAsset(String key) throws URISyntaxException, IOException { @@ -193,11 +224,13 @@ private boolean isCachedClientSide(HttpServletRequest req, CachedResource cached // header field String ifNoneMatch = req.getHeader(HttpHeaders.IF_NONE_MATCH); String eTag = cachedAsset.getETag(); - if (Objects.nonNull(ifNoneMatch) && Objects.nonNull(eTag)) return eTag.equals(ifNoneMatch); + if (Objects.nonNull(ifNoneMatch) && Objects.nonNull(eTag)) { + return eTag.equals(ifNoneMatch); + } // HTTP: A recipient MUST ignore the If-Modified-Since header field if the request method is // neither GET nor HEAD. - String method = req.getMethod().toUpperCase(); - if (method.equals("GET") || method.equals("HEAD")) { + String method = req.getMethod().toUpperCase(Locale.ROOT); + if ("GET".equals(method) || "HEAD".equals(method)) { try { long ifModifiedSince = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE); return ifModifiedSince >= cachedAsset.getLastModified(); diff --git a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/URICustomizer.java b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/URICustomizer.java index ce2872fe..44b684c7 100644 --- a/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/URICustomizer.java +++ b/xtraplatform-web/src/main/java/de/ii/xtraplatform/web/domain/URICustomizer.java @@ -157,48 +157,52 @@ public URICustomizer cutPathAfterSegments(final String... segments) { } final List pathSegments = getPathSegments(); - int pathSegmentsIndex = -1; + int lastMatchIndex = findLastMatchingSegmentIndex(pathSegments, segments); + + if (lastMatchIndex == -1) { + return this; + } + + int cutIndex = determineCutIndex(pathSegments, segments, lastMatchIndex); + this.setPathSegments( + new ImmutableList.Builder().addAll(pathSegments.subList(0, cutIndex)).build()); + + return this; + } + + private int findLastMatchingSegmentIndex(List pathSegments, String... segments) { + int lastMatchIndex = -1; for (int i = 0; i <= pathSegments.size() - segments.length; i++) { - boolean found = true; - for (int j = 0; j < segments.length; j++) { - if (!pathSegments.get(i + j).equals(segments[j])) { - found = false; - break; - } - } - if (found) { - pathSegmentsIndex = i; + if (segmentsMatchAt(pathSegments, segments, i)) { + lastMatchIndex = i; } } - if (pathSegmentsIndex == -1) { - return this; + return lastMatchIndex; + } + + private boolean segmentsMatchAt(List pathSegments, String[] segments, int startIndex) { + for (int j = 0; j < segments.length; j++) { + if (!pathSegments.get(startIndex + j).equals(segments[j])) { + return false; + } } + return true; + } - boolean match = true; - int cutIndex = 0; - for (int k = pathSegmentsIndex; k < pathSegments.size(); k++) { + private int determineCutIndex(List pathSegments, String[] segments, int startIndex) { + for (int k = startIndex; k < pathSegments.size(); k++) { for (int l = 0; k + l < pathSegments.size() && l < segments.length; l++) { if (!pathSegments.get(k + l).equals(segments[l])) { - match = false; - break; + return pathSegments.size(); // No match found, return full size + } + if (l == segments.length - 1) { + return k + l + 1; // Found complete match, cut after this } - cutIndex = k + l + 1; - } - if (match) { - break; - } else { - match = true; } } - - int segmentsIndex = match ? cutIndex : pathSegments.size(); - - this.setPathSegments( - new ImmutableList.Builder().addAll(pathSegments.subList(0, segmentsIndex)).build()); - - return this; + return pathSegments.size(); } public URICustomizer removePathSegment(final String segment, final int index) {