ServerHeaderCustomizer.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.configuration;
import lombok.extern.slf4j.Slf4j;
import org.entrystore.impl.RepositoryManagerImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Slf4j
@Component
public class ServerHeaderCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
/**
* Controls how much of the EntryStore version appears in the {@code Server}
* response header. Naming follows Apache httpd's {@code ServerTokens}
* (MAJOR = "1 segment", MINOR = "2 segments") — NOT semver ordering.
*/
enum VersionPrecision {
/** Full version, e.g. {@code 6.0-SNAPSHOT}. */ FULL(-1),
/** Major + minor, e.g. {@code 6.0}. */ MINOR(2),
/** Major only, e.g. {@code 6}. */ MAJOR(1),
/** Suppress version entirely, header is {@code EntryStore}. */ NONE(0);
final int segments;
VersionPrecision(int segments) {
this.segments = segments;
}
}
@Value("${entrystore.http.header.server:}")
private String configuredServerHeader;
@Value("${entrystore.http.header.server.version-precision:full}")
private VersionPrecision versionPrecision;
@Override
public void customize(ConfigurableWebServerFactory factory) {
String value = resolveServerHeader(configuredServerHeader, versionPrecision, RepositoryManagerImpl::getVersion);
factory.setServerHeader(value);
log.info("Server response header set to: {}", value);
}
static String resolveServerHeader(String override, VersionPrecision precision, Supplier<String> versionSupplier) {
if (override != null && !override.isBlank()) {
return override;
}
try {
return composeDefault(versionSupplier.get(), precision);
} catch (RuntimeException e) {
// Server header is non-essential to startup; degrade gracefully so a version-loading
// glitch never prevents Jetty from coming up.
log.error("Failed to compose default Server header (precision={}), falling back to 'EntryStore'",
precision, e);
return "EntryStore";
}
}
static String composeDefault(String rawVersion, VersionPrecision precision) {
String suffix = truncateVersion(rawVersion, precision);
return suffix.isEmpty() ? "EntryStore" : "EntryStore/" + suffix;
}
static String truncateVersion(String version, VersionPrecision precision) {
if (version == null || version.isBlank()) {
return "";
}
return switch (precision) {
case FULL -> version;
case NONE -> "";
case MAJOR, MINOR -> keepNumericSegments(version, precision.segments);
};
}
// Strip Maven qualifier (e.g. "-SNAPSHOT", "+build.1") before splitting so segments are digit-only.
private static String keepNumericSegments(String version, int segments) {
return Arrays.stream(version.replaceAll("[-+].*$", "").split("\\."))
.limit(segments)
.map(s -> s.replaceAll("[^\\d].*", ""))
.filter(s -> !s.isEmpty())
.collect(Collectors.joining("."));
}
}