JerseySeBootstrapConfiguration.java
/*
* Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.server;
import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.core.UriBuilder;
import org.glassfish.jersey.internal.config.ExternalPropertiesConfigurationFactory;
import org.glassfish.jersey.internal.config.SystemPropertiesConfigurationModel;
import org.glassfish.jersey.internal.util.PropertiesClass;
import org.glassfish.jersey.server.internal.LocalizationMessages;
import org.glassfish.jersey.server.spi.Container;
import org.glassfish.jersey.server.spi.WebServer;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.function.BiFunction;
import java.util.logging.Logger;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
/**
* Jersey implementation of {@link SeBootstrap.Configuration} implementing arbitrary methods for acquiring
* the configuration settings.
* @since 3.1.0
*/
public final class JerseySeBootstrapConfiguration implements SeBootstrap.Configuration {
private static final Logger LOGGER = Logger.getLogger(JerseySeBootstrapConfiguration.class.getName());
protected static final Random RANDOM = new Random();
private final SeBootstrap.Configuration configuration;
private JerseySeBootstrapConfiguration(SeBootstrap.Configuration configuration) {
this.configuration = configuration;
}
@Override
public Object property(String name) {
return configuration.property(name);
}
/**
* Compose {@link URI} based on properties defined in this configuration.
* @param resolveDefaultPort if {@code true} the port is not set, it is resolved as
* {@link Container#DEFAULT_HTTP_PORT} or {@link Container#DEFAULT_HTTPS_PORT}
* based on the protocol scheme.
* @return Composed {@link URI} based on properties defined in this configuration.
*/
public URI uri(boolean resolveDefaultPort) {
final String protocol = configuration.protocol();
final String host = configuration.host();
final int port = resolveDefaultPort ? resolvePort() : configuration.port();
final String rootPath = configuration.rootPath();
final URI uri = UriBuilder.newInstance().scheme(protocol.toLowerCase()).host(host).port(port).path(rootPath)
.build();
return uri;
}
private int resolvePort() {
final int configPort = configuration.port();
final int basePort = allowPrivilegedPorts() ? 0 : 8000;
final int port;
switch (configPort) {
case SeBootstrap.Configuration.DEFAULT_PORT:
port = basePort + (isHttps() ? Container.DEFAULT_HTTPS_PORT : Container.DEFAULT_HTTP_PORT);
break;
case SeBootstrap.Configuration.FREE_PORT:
port = _resolvePort(basePort == 0);
break;
default:
port = configPort;
break;
}
return port;
}
private int _resolvePort(boolean allowPrivilegedPort) {
final int basePort = allowPrivilegedPort ? 0 : 1024;
// Get the initial range parameters
final int lower = basePort;
final int range = 0xFFFF;
// Select a start point in the range
final int initialOffset = RANDOM.nextInt(range - lower);
// Loop the offset through all ports in the range and attempt
// to bind to each
int offset = initialOffset;
ServerSocket socket;
do {
final int port = lower + offset;
try {
socket = new ServerSocket(port);
socket.close();
return port;
} catch (IOException caught) {
// Swallow exceptions until the end
}
offset = (offset + 1) % range;
} while (offset != initialOffset);
// If a port can't be bound, throw the exception
throw new IllegalArgumentException(LocalizationMessages.COULD_NOT_BIND_TO_ANY_PORT());
}
/**
* Return {@link SSLContext} in the configuration if the protocol scheme is {@code HTTPS}.
* @return the SSLContext in the configuration.
*/
@Override
public SSLContext sslContext() {
final SSLContext sslContext = configuration.sslContext();
return isHttps() ? sslContext : null;
}
/**
* If the protocol schema is {@code HTTPS}, return {@code true}.
* @return {@code true} when the protocol schema is {@code HTTPS}.
*/
public boolean isHttps() {
return "HTTPS".equalsIgnoreCase(configuration.protocol());
}
/**
* Defines if the {@link WebServer} should automatically start.
* @return false if {@link ServerProperties#WEBSERVER_AUTO_START} is {@code false}, {@code true} otherwise.
*/
public boolean autoStart() {
final boolean autoStart = Optional.ofNullable(
(Boolean) configuration.property(ServerProperties.WEBSERVER_AUTO_START))
.orElse(TRUE);
return autoStart;
}
/**
* Defines if the {@link WebServer} should start on a privileged port when port is not set.
* @return true if {@link ServerProperties#WEBSERVER_AUTO_START} is {@code true}, {@code false} otherwise.
*/
public boolean allowPrivilegedPorts() {
return Optional.ofNullable(
(Boolean) configuration.property(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS))
.orElse(FALSE);
}
/**
* Factory method creating {@code JerseySeBootstrapConfiguration} wrapper around {@link SeBootstrap.Configuration}.
* @param configuration wrapped configuration
* @return {@code JerseySeBootstrapConfiguration} wrapper around {@link SeBootstrap.Configuration}.
*/
public static JerseySeBootstrapConfiguration from(SeBootstrap.Configuration configuration) {
return JerseySeBootstrapConfiguration.class.isInstance(configuration)
? (JerseySeBootstrapConfiguration) configuration
: new JerseySeBootstrapConfiguration(configuration);
}
/**
* Return a Jersey instance of {@link SeBootstrap.Configuration.Builder} with prefilled values.
* @return a Jersey instance of {@link SeBootstrap.Configuration.Builder}.
*/
public static Builder builder() {
return new Builder();
}
public static final class Builder implements SeBootstrap.Configuration.Builder {
private static final Map<String, Class<?>> PROPERTY_TYPES = new HashMap<>();
static {
PROPERTY_TYPES.put(SeBootstrap.Configuration.PROTOCOL, String.class);
PROPERTY_TYPES.put(SeBootstrap.Configuration.HOST, String.class);
PROPERTY_TYPES.put(SeBootstrap.Configuration.PORT, Integer.class);
PROPERTY_TYPES.put(SeBootstrap.Configuration.ROOT_PATH, String.class);
PROPERTY_TYPES.put(SeBootstrap.Configuration.SSL_CONTEXT, SSLContext.class);
PROPERTY_TYPES.put(SeBootstrap.Configuration.SSL_CLIENT_AUTHENTICATION, SSLClientAuthentication.class);
PROPERTY_TYPES.put(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS, Boolean.class);
PROPERTY_TYPES.put(ServerProperties.WEBSERVER_AUTO_START, Boolean.class);
PROPERTY_TYPES.put(ServerProperties.WEBSERVER_CLASS, Class.class);
}
private final Map<String, Object> properties = new HashMap<>();
private Builder() {
this.properties.put(SeBootstrap.Configuration.PROTOCOL, "HTTP"); // upper case mandated by javadoc
this.properties.put(SeBootstrap.Configuration.HOST, "localhost");
this.properties.put(SeBootstrap.Configuration.PORT, -1); // Auto-select port 8080 for HTTP or 8443 for HTTPS
this.properties.put(SeBootstrap.Configuration.ROOT_PATH, "/");
this.properties.put(ServerProperties.WEBSERVER_CLASS, WebServer.class); // Auto-select first provider
try {
this.properties.put(SeBootstrap.Configuration.SSL_CONTEXT, SSLContext.getDefault());
} catch (final NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
this.properties.put(SeBootstrap.Configuration.SSL_CLIENT_AUTHENTICATION,
SeBootstrap.Configuration.SSLClientAuthentication.NONE);
this.properties.put(ServerProperties.WEBSERVER_AUTO_START, TRUE);
this.properties.put(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS, FALSE);
SystemPropertiesConfigurationModel propertiesConfigurationModel = new SystemPropertiesConfigurationModel(
Collections.singletonList(Properties.class.getName())
);
from((name, aClass) -> String.class.equals(aClass) || Integer.class.equals(aClass) || Boolean.class.equals(aClass)
? propertiesConfigurationModel.getOptionalProperty(name, aClass)
: Optional.empty()
);
}
@Override
public JerseySeBootstrapConfiguration build() {
return JerseySeBootstrapConfiguration.from(this.properties::get);
}
@Override
public Builder property(String name, Object value) {
this.properties.put(name, value);
return this;
}
/**
* Set the the respective {@link WebServer} class to be used by the
* {@link org.glassfish.jersey.server.spi.WebServerProvider}.
* @param webServerClass the class implementing {@link WebServer}.
* @return the updated builder.
*/
public Builder webServerClass(Class<? extends WebServer> webServerClass) {
return property(ServerProperties.WEBSERVER_CLASS, webServerClass);
}
/**
* Define if the {@link WebServer} should auto-start at bootstrap.
* @param autostart the auto-start flag.
* @return the updated builder.
*/
public Builder autoStart(Boolean autostart) {
return property(ServerProperties.WEBSERVER_AUTO_START, autostart);
}
@Override
public <T> JerseySeBootstrapConfiguration.Builder from(BiFunction<String, Class<T>, Optional<T>> configProvider) {
PROPERTY_TYPES.forEach(
(propertyName, propertyType) -> configProvider.apply(propertyName, (Class<T>) propertyType)
.ifPresent(propertyValue -> this.properties.put(propertyName, propertyValue)));
return this;
}
@Override
public JerseySeBootstrapConfiguration.Builder from(Object externalConfig) {
if (SeBootstrap.Configuration.class.isInstance(externalConfig)) {
final SeBootstrap.Configuration other = (SeBootstrap.Configuration) externalConfig;
from((name, clazz) -> {
final Object property = other.property(name);
if (property != null) {
if (clazz.equals(property.getClass())) {
return Optional.of(property);
} else {
LOGGER.warning(LocalizationMessages.IGNORE_SEBOOTSTRAP_CONFIGURATION_PROPERTY(name, clazz));
}
}
return Optional.empty();
});
}
return this;
}
}
/**
* Name the properties to be internally read from System properties by {@link ExternalPropertiesConfigurationFactory}.
* This is required just when SecurityManager is on, otherwise all system properties are read.
*/
@PropertiesClass
private static class Properties {
/**
* See {@link SeBootstrap.Configuration#PROTOCOL} property.
*/
public static final String SE_BOOTSTRAP_CONFIGURATION_PROTOCOL = SeBootstrap.Configuration.PROTOCOL;
/**
* See {@link SeBootstrap.Configuration#HOST} property.
*/
public static final String SE_BOOTSTRAP_CONFIGURATION_HOST = SeBootstrap.Configuration.HOST;
/**
* See {@link SeBootstrap.Configuration#PORT} property.
*/
public static final String SE_BOOTSTRAP_CONFIGURATION_PORT = SeBootstrap.Configuration.PORT;
/**
* See {@link SeBootstrap.Configuration#ROOT_PATH} property.
*/
public static final String SE_BOOTSTRAP_CONFIGURATION_ROOT_PATH = SeBootstrap.Configuration.ROOT_PATH;
/**
* See {@link ServerProperties#WEBSERVER_ALLOW_PRIVILEGED_PORTS} property.
*/
public static final String WEBSERVER_ALLOW_PRIVILEGED_PORTS = ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS;
/**
* See {@link ServerProperties#WEBSERVER_AUTO_START} property.
*/
public static final String WEBSERVER_AUTO_START = ServerProperties.WEBSERVER_AUTO_START;
}
}